001/*
002 * Copyright (c) 2003 Objectix Pty Ltd  All rights reserved.
003 *
004 * This library is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU Lesser General Public
006 * License as published by the Free Software Foundation.
007 *
008 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
009 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
010 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
011 * DISCLAIMED.  IN NO EVENT SHALL OBJECTIX PTY LTD BE LIABLE FOR ANY
012 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
013 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
014 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
015 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
016 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
017 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
018 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
019 */
020package org.openstreetmap.josm.data.projection.datum;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.Serializable;
025import java.nio.charset.StandardCharsets;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.List;
029
030/**
031 * Models the NTv2 format Grid Shift File and exposes methods to shift
032 * coordinate values using the Sub Grids contained in the file.
033 * <p>The principal reference for the alogrithms used is the
034 * 'GDAit Software Architecture Manual' produced by the <a
035 * href='http://www.sli.unimelb.edu.au/gda94'>Geomatics
036 * Department of the University of Melbourne</a>
037 * <p>This library reads binary NTv2 Grid Shift files in Big Endian
038 * (Canadian standard) or Little Endian (Australian Standard) format.
039 * The older 'Australian' binary format is not supported, only the
040 * official Canadian format, which is now also used for the national
041 * Australian Grid.
042 * <p>Grid Shift files can be read as InputStreams or RandomAccessFiles.
043 * Loading an InputStream places all the required node information
044 * (accuracy data is optional) into heap based Java arrays. This is the
045 * highest perfomance option, and is useful for large volume transformations.
046 * Non-file data sources (eg using an SQL Blob) are also supported through
047 * InputStream. The RandonAccessFile option has a much smaller memory
048 * footprint as only the Sub Grid headers are stored in memory, but
049 * transformation is slower because the file must be read a number of
050 * times for each transformation.
051 * <p>Coordinates may be shifted Forward (ie from and to the Datums specified
052 * in the Grid Shift File header) or Reverse. The reverse transformation
053 * uses an iterative approach to approximate the Grid Shift, as the
054 * precise transformation is based on 'from' datum coordinates.
055 * <p>Coordinates may be specified
056 * either in Seconds using Positive West Longitude (the original NTv2
057 * arrangement) or in decimal Degrees using Positive East Longitude.
058 *
059 * @author Peter Yuill
060 * Modifified for JOSM :
061 * - removed the RandomAccessFile mode (Pieren)
062 */
063public class NTV2GridShiftFile implements Serializable {
064
065    private int overviewHeaderCount;
066    private int subGridHeaderCount;
067    private int subGridCount;
068    private String shiftType;
069    private String version;
070    private String fromEllipsoid = "";
071    private String toEllipsoid = "";
072    private double fromSemiMajorAxis;
073    private double fromSemiMinorAxis;
074    private double toSemiMajorAxis;
075    private double toSemiMinorAxis;
076
077    private NTV2SubGrid[] topLevelSubGrid;
078    private NTV2SubGrid lastSubGrid;
079
080    /**
081     * Constructs a new {@code NTV2GridShiftFile}.
082     */
083    public NTV2GridShiftFile() {
084    }
085
086    /**
087     * Load a Grid Shift File from an InputStream. The Grid Shift node
088     * data is stored in Java arrays, which will occupy about the same memory
089     * as the original file with accuracy data included, and about half that
090     * with accuracy data excluded. The size of the Australian national file
091     * is 4.5MB, and the Canadian national file is 13.5MB
092     * <p>The InputStream is closed by this method.
093     *
094     * @param in Grid Shift File InputStream
095     * @param loadAccuracy is Accuracy data to be loaded as well as shift data?
096     * @throws IOException
097     */
098    public void loadGridShiftFile(InputStream in, boolean loadAccuracy ) throws IOException {
099        byte[] b8 = new byte[8];
100        boolean bigEndian = true;
101        fromEllipsoid = "";
102        toEllipsoid = "";
103        topLevelSubGrid = null;
104        in.read(b8);
105        String overviewHeaderCountId = new String(b8, StandardCharsets.UTF_8);
106        if (!"NUM_OREC".equals(overviewHeaderCountId))
107            throw new IllegalArgumentException("Input file is not an NTv2 grid shift file");
108        in.read(b8);
109        overviewHeaderCount = NTV2Util.getIntBE(b8, 0);
110        if (overviewHeaderCount == 11) {
111            bigEndian = true;
112        } else {
113            overviewHeaderCount = NTV2Util.getIntLE(b8, 0);
114            if (overviewHeaderCount == 11) {
115                bigEndian = false;
116            } else
117                throw new IllegalArgumentException("Input file is not an NTv2 grid shift file");
118        }
119        in.read(b8);
120        in.read(b8);
121        subGridHeaderCount = NTV2Util.getInt(b8, bigEndian);
122        in.read(b8);
123        in.read(b8);
124        subGridCount = NTV2Util.getInt(b8, bigEndian);
125        NTV2SubGrid[] subGrid = new NTV2SubGrid[subGridCount];
126        in.read(b8);
127        in.read(b8);
128        shiftType = new String(b8, StandardCharsets.UTF_8);
129        in.read(b8);
130        in.read(b8);
131        version = new String(b8, StandardCharsets.UTF_8);
132        in.read(b8);
133        in.read(b8);
134        fromEllipsoid = new String(b8, StandardCharsets.UTF_8);
135        in.read(b8);
136        in.read(b8);
137        toEllipsoid = new String(b8, StandardCharsets.UTF_8);
138        in.read(b8);
139        in.read(b8);
140        fromSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian);
141        in.read(b8);
142        in.read(b8);
143        fromSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian);
144        in.read(b8);
145        in.read(b8);
146        toSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian);
147        in.read(b8);
148        in.read(b8);
149        toSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian);
150
151        for (int i = 0; i < subGridCount; i++) {
152            subGrid[i] = new NTV2SubGrid(in, bigEndian, loadAccuracy);
153        }
154        topLevelSubGrid = createSubGridTree(subGrid);
155        lastSubGrid = topLevelSubGrid[0];
156    }
157
158    /**
159     * Create a tree of Sub Grids by adding each Sub Grid to its parent (where
160     * it has one), and returning an array of the top level Sub Grids
161     * @param subGrid an array of all Sub Grids
162     * @return an array of top level Sub Grids with lower level Sub Grids set.
163     */
164    private NTV2SubGrid[] createSubGridTree(NTV2SubGrid[] subGrid) {
165        int topLevelCount = 0;
166        HashMap<String, List<NTV2SubGrid>> subGridMap = new HashMap<>();
167        for (int i = 0; i < subGrid.length; i++) {
168            if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) {
169                topLevelCount++;
170            }
171            subGridMap.put(subGrid[i].getSubGridName(), new ArrayList<NTV2SubGrid>());
172        }
173        NTV2SubGrid[] topLevelSubGrid = new NTV2SubGrid[topLevelCount];
174        topLevelCount = 0;
175        for (int i = 0; i < subGrid.length; i++) {
176            if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) {
177                topLevelSubGrid[topLevelCount++] = subGrid[i];
178            } else {
179                List<NTV2SubGrid> parent = subGridMap.get(subGrid[i].getParentSubGridName());
180                parent.add(subGrid[i]);
181            }
182        }
183        NTV2SubGrid[] nullArray = new NTV2SubGrid[0];
184        for (int i = 0; i < subGrid.length; i++) {
185            List<NTV2SubGrid> subSubGrids = subGridMap.get(subGrid[i].getSubGridName());
186            if (!subSubGrids.isEmpty()) {
187                NTV2SubGrid[] subGridArray = subSubGrids.toArray(nullArray);
188                subGrid[i].setSubGridArray(subGridArray);
189            }
190        }
191        return topLevelSubGrid;
192    }
193
194    /**
195     * Shift a coordinate in the Forward direction of the Grid Shift File.
196     *
197     * @param gs A GridShift object containing the coordinate to shift
198     * @return True if the coordinate is within a Sub Grid, false if not
199     */
200    public boolean gridShiftForward(NTV2GridShift gs) {
201        // Try the last sub grid first, big chance the coord is still within it
202        NTV2SubGrid subGrid = lastSubGrid.getSubGridForCoord(gs.getLonPositiveWestSeconds(), gs.getLatSeconds());
203        if (subGrid == null) {
204            subGrid = getSubGrid(gs.getLonPositiveWestSeconds(), gs.getLatSeconds());
205        }
206        if (subGrid == null)
207            return false;
208        else {
209            subGrid.interpolateGridShift(gs);
210            gs.setSubGridName(subGrid.getSubGridName());
211            lastSubGrid = subGrid;
212            return true;
213        }
214    }
215
216    /**
217     * Shift a coordinate in the Reverse direction of the Grid Shift File.
218     *
219     * @param gs A GridShift object containing the coordinate to shift
220     * @return True if the coordinate is within a Sub Grid, false if not
221     */
222    public boolean gridShiftReverse(NTV2GridShift gs) {
223        // set up the first estimate
224        NTV2GridShift forwardGs = new NTV2GridShift();
225        forwardGs.setLonPositiveWestSeconds(gs.getLonPositiveWestSeconds());
226        forwardGs.setLatSeconds(gs.getLatSeconds());
227        for (int i = 0; i < 4; i++) {
228            if (!gridShiftForward(forwardGs))
229                return false;
230            forwardGs.setLonPositiveWestSeconds(
231                    gs.getLonPositiveWestSeconds() - forwardGs.getLonShiftPositiveWestSeconds());
232            forwardGs.setLatSeconds(gs.getLatSeconds() - forwardGs.getLatShiftSeconds());
233        }
234        gs.setLonShiftPositiveWestSeconds(-forwardGs.getLonShiftPositiveWestSeconds());
235        gs.setLatShiftSeconds(-forwardGs.getLatShiftSeconds());
236        gs.setLonAccuracyAvailable(forwardGs.isLonAccuracyAvailable());
237        if (forwardGs.isLonAccuracyAvailable()) {
238            gs.setLonAccuracySeconds(forwardGs.getLonAccuracySeconds());
239        }
240        gs.setLatAccuracyAvailable(forwardGs.isLatAccuracyAvailable());
241        if (forwardGs.isLatAccuracyAvailable()) {
242            gs.setLatAccuracySeconds(forwardGs.getLatAccuracySeconds());
243        }
244        return true;
245    }
246
247    /**
248     * Find the finest SubGrid containing the coordinate, specified
249     * in Positive West Seconds
250     *
251     * @param lon Longitude in Positive West Seconds
252     * @param lat Latitude in Seconds
253     * @return The SubGrid found or null
254     */
255    private NTV2SubGrid getSubGrid(double lon, double lat) {
256        NTV2SubGrid sub = null;
257        for (int i = 0; i < topLevelSubGrid.length; i++) {
258            sub = topLevelSubGrid[i].getSubGridForCoord(lon, lat);
259            if (sub != null) {
260                break;
261            }
262        }
263        return sub;
264    }
265
266    public boolean isLoaded() {
267        return (topLevelSubGrid != null);
268    }
269
270    public void unload() {
271        topLevelSubGrid = null;
272    }
273
274    @Override
275    public String toString() {
276        StringBuilder buf = new StringBuilder("Headers  : ");
277        buf.append(overviewHeaderCount);
278        buf.append("\nSub Hdrs : ");
279        buf.append(subGridHeaderCount);
280        buf.append("\nSub Grids: ");
281        buf.append(subGridCount);
282        buf.append("\nType     : ");
283        buf.append(shiftType);
284        buf.append("\nVersion  : ");
285        buf.append(version);
286        buf.append("\nFr Ellpsd: ");
287        buf.append(fromEllipsoid);
288        buf.append("\nTo Ellpsd: ");
289        buf.append(toEllipsoid);
290        buf.append("\nFr Maj Ax: ");
291        buf.append(fromSemiMajorAxis);
292        buf.append("\nFr Min Ax: ");
293        buf.append(fromSemiMinorAxis);
294        buf.append("\nTo Maj Ax: ");
295        buf.append(toSemiMajorAxis);
296        buf.append("\nTo Min Ax: ");
297        buf.append(toSemiMinorAxis);
298        return buf.toString();
299    }
300
301    /**
302     * Get a copy of the SubGrid tree for this file.
303     *
304     * @return a deep clone of the current SubGrid tree
305     */
306    public NTV2SubGrid[] getSubGridTree() {
307        NTV2SubGrid[] clone = new NTV2SubGrid[topLevelSubGrid.length];
308        for (int i = 0; i < topLevelSubGrid.length; i++) {
309            clone[i] = (NTV2SubGrid)topLevelSubGrid[i].clone();
310        }
311        return clone;
312    }
313
314    public String getFromEllipsoid() {
315        return fromEllipsoid;
316    }
317
318    public String getToEllipsoid() {
319        return toEllipsoid;
320    }
321
322}