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;
029import java.util.Map;
030
031import org.openstreetmap.josm.Main;
032
033/**
034 * Models the NTv2 format Grid Shift File and exposes methods to shift
035 * coordinate values using the Sub Grids contained in the file.
036 * <p>The principal reference for the alogrithms used is the
037 * 'GDAit Software Architecture Manual' produced by the <a
038 * href='http://www.sli.unimelb.edu.au/gda94'>Geomatics
039 * Department of the University of Melbourne</a>
040 * <p>This library reads binary NTv2 Grid Shift files in Big Endian
041 * (Canadian standard) or Little Endian (Australian Standard) format.
042 * The older 'Australian' binary format is not supported, only the
043 * official Canadian format, which is now also used for the national
044 * Australian Grid.
045 * <p>Grid Shift files can be read as InputStreams or RandomAccessFiles.
046 * Loading an InputStream places all the required node information
047 * (accuracy data is optional) into heap based Java arrays. This is the
048 * highest perfomance option, and is useful for large volume transformations.
049 * Non-file data sources (eg using an SQL Blob) are also supported through
050 * InputStream. The RandonAccessFile option has a much smaller memory
051 * footprint as only the Sub Grid headers are stored in memory, but
052 * transformation is slower because the file must be read a number of
053 * times for each transformation.
054 * <p>Coordinates may be shifted Forward (ie from and to the Datums specified
055 * in the Grid Shift File header) or Reverse. The reverse transformation
056 * uses an iterative approach to approximate the Grid Shift, as the
057 * precise transformation is based on 'from' datum coordinates.
058 * <p>Coordinates may be specified
059 * either in Seconds using Positive West Longitude (the original NTv2
060 * arrangement) or in decimal Degrees using Positive East Longitude.
061 *
062 * @author Peter Yuill
063 * Modified for JOSM :
064 * - removed the RandomAccessFile mode (Pieren)
065 */
066public class NTV2GridShiftFile implements Serializable {
067
068    private static final long serialVersionUID = 1L;
069
070    private int overviewHeaderCount;
071    private int subGridHeaderCount;
072    private int subGridCount;
073    private String shiftType;
074    private String version;
075    private String fromEllipsoid = "";
076    private String toEllipsoid = "";
077    private double fromSemiMajorAxis;
078    private double fromSemiMinorAxis;
079    private double toSemiMajorAxis;
080    private double toSemiMinorAxis;
081
082    private NTV2SubGrid[] topLevelSubGrid;
083    private NTV2SubGrid lastSubGrid;
084
085    private static void readBytes(InputStream in, byte[] b) throws IOException {
086        if (in.read(b) < b.length) {
087            Main.error("Failed to read expected amount of bytes ("+ b.length +") from stream");
088        }
089    }
090
091    /**
092     * Load a Grid Shift File from an InputStream. The Grid Shift node
093     * data is stored in Java arrays, which will occupy about the same memory
094     * as the original file with accuracy data included, and about half that
095     * with accuracy data excluded. The size of the Australian national file
096     * is 4.5MB, and the Canadian national file is 13.5MB
097     * <p>The InputStream is closed by this method.
098     *
099     * @param in Grid Shift File InputStream
100     * @param loadAccuracy is Accuracy data to be loaded as well as shift data?
101     * @throws IOException if any I/O error occurs
102     */
103    public void loadGridShiftFile(InputStream in, boolean loadAccuracy) throws IOException {
104        byte[] b8 = new byte[8];
105        fromEllipsoid = "";
106        toEllipsoid = "";
107        topLevelSubGrid = null;
108        readBytes(in, b8);
109        String overviewHeaderCountId = new String(b8, StandardCharsets.UTF_8);
110        if (!"NUM_OREC".equals(overviewHeaderCountId))
111            throw new IllegalArgumentException("Input file is not an NTv2 grid shift file");
112        boolean bigEndian;
113        readBytes(in, b8);
114        overviewHeaderCount = NTV2Util.getIntBE(b8, 0);
115        if (overviewHeaderCount == 11) {
116            bigEndian = true;
117        } else {
118            overviewHeaderCount = NTV2Util.getIntLE(b8, 0);
119            if (overviewHeaderCount == 11) {
120                bigEndian = false;
121            } else
122                throw new IllegalArgumentException("Input file is not an NTv2 grid shift file");
123        }
124        readBytes(in, b8);
125        readBytes(in, b8);
126        subGridHeaderCount = NTV2Util.getInt(b8, bigEndian);
127        readBytes(in, b8);
128        readBytes(in, b8);
129        subGridCount = NTV2Util.getInt(b8, bigEndian);
130        NTV2SubGrid[] subGrid = new NTV2SubGrid[subGridCount];
131        readBytes(in, b8);
132        readBytes(in, b8);
133        shiftType = new String(b8, StandardCharsets.UTF_8);
134        readBytes(in, b8);
135        readBytes(in, b8);
136        version = new String(b8, StandardCharsets.UTF_8);
137        readBytes(in, b8);
138        readBytes(in, b8);
139        fromEllipsoid = new String(b8, StandardCharsets.UTF_8);
140        readBytes(in, b8);
141        readBytes(in, b8);
142        toEllipsoid = new String(b8, StandardCharsets.UTF_8);
143        readBytes(in, b8);
144        readBytes(in, b8);
145        fromSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian);
146        readBytes(in, b8);
147        readBytes(in, b8);
148        fromSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian);
149        readBytes(in, b8);
150        readBytes(in, b8);
151        toSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian);
152        readBytes(in, b8);
153        readBytes(in, b8);
154        toSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian);
155
156        for (int i = 0; i < subGridCount; i++) {
157            subGrid[i] = new NTV2SubGrid(in, bigEndian, loadAccuracy);
158        }
159        topLevelSubGrid = createSubGridTree(subGrid);
160        lastSubGrid = topLevelSubGrid[0];
161    }
162
163    /**
164     * Create a tree of Sub Grids by adding each Sub Grid to its parent (where
165     * it has one), and returning an array of the top level Sub Grids
166     * @param subGrid an array of all Sub Grids
167     * @return an array of top level Sub Grids with lower level Sub Grids set.
168     */
169    private static NTV2SubGrid[] createSubGridTree(NTV2SubGrid ... subGrid) {
170        int topLevelCount = 0;
171        Map<String, List<NTV2SubGrid>> subGridMap = new HashMap<>();
172        for (int i = 0; i < subGrid.length; i++) {
173            if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) {
174                topLevelCount++;
175            }
176            subGridMap.put(subGrid[i].getSubGridName(), new ArrayList<NTV2SubGrid>());
177        }
178        NTV2SubGrid[] topLevelSubGrid = new NTV2SubGrid[topLevelCount];
179        topLevelCount = 0;
180        for (int i = 0; i < subGrid.length; i++) {
181            if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) {
182                topLevelSubGrid[topLevelCount++] = subGrid[i];
183            } else {
184                List<NTV2SubGrid> parent = subGridMap.get(subGrid[i].getParentSubGridName());
185                parent.add(subGrid[i]);
186            }
187        }
188        NTV2SubGrid[] nullArray = new NTV2SubGrid[0];
189        for (int i = 0; i < subGrid.length; i++) {
190            List<NTV2SubGrid> subSubGrids = subGridMap.get(subGrid[i].getSubGridName());
191            if (!subSubGrids.isEmpty()) {
192                NTV2SubGrid[] subGridArray = subSubGrids.toArray(nullArray);
193                subGrid[i].setSubGridArray(subGridArray);
194            }
195        }
196        return topLevelSubGrid;
197    }
198
199    /**
200     * Shift a coordinate in the Forward direction of the Grid Shift File.
201     *
202     * @param gs A GridShift object containing the coordinate to shift
203     * @return True if the coordinate is within a Sub Grid, false if not
204     */
205    public boolean gridShiftForward(NTV2GridShift gs) {
206        NTV2SubGrid subGrid = null;
207        if (lastSubGrid != null) {
208            // Try the last sub grid first, big chance the coord is still within it
209            subGrid = lastSubGrid.getSubGridForCoord(gs.getLonPositiveWestSeconds(), gs.getLatSeconds());
210        }
211        if (subGrid == null) {
212            subGrid = getSubGrid(topLevelSubGrid, gs.getLonPositiveWestSeconds(), gs.getLatSeconds());
213        }
214        if (subGrid == null) {
215            return false;
216        } else {
217            subGrid.interpolateGridShift(gs);
218            gs.setSubGridName(subGrid.getSubGridName());
219            lastSubGrid = subGrid;
220            return true;
221        }
222    }
223
224    /**
225     * Shift a coordinate in the Reverse direction of the Grid Shift File.
226     *
227     * @param gs A GridShift object containing the coordinate to shift
228     * @return True if the coordinate is within a Sub Grid, false if not
229     */
230    public boolean gridShiftReverse(NTV2GridShift gs) {
231        // set up the first estimate
232        NTV2GridShift forwardGs = new NTV2GridShift();
233        forwardGs.setLonPositiveWestSeconds(gs.getLonPositiveWestSeconds());
234        forwardGs.setLatSeconds(gs.getLatSeconds());
235        for (int i = 0; i < 4; i++) {
236            if (!gridShiftForward(forwardGs))
237                return false;
238            forwardGs.setLonPositiveWestSeconds(
239                    gs.getLonPositiveWestSeconds() - forwardGs.getLonShiftPositiveWestSeconds());
240            forwardGs.setLatSeconds(gs.getLatSeconds() - forwardGs.getLatShiftSeconds());
241        }
242        gs.setLonShiftPositiveWestSeconds(-forwardGs.getLonShiftPositiveWestSeconds());
243        gs.setLatShiftSeconds(-forwardGs.getLatShiftSeconds());
244        gs.setLonAccuracyAvailable(forwardGs.isLonAccuracyAvailable());
245        if (forwardGs.isLonAccuracyAvailable()) {
246            gs.setLonAccuracySeconds(forwardGs.getLonAccuracySeconds());
247        }
248        gs.setLatAccuracyAvailable(forwardGs.isLatAccuracyAvailable());
249        if (forwardGs.isLatAccuracyAvailable()) {
250            gs.setLatAccuracySeconds(forwardGs.getLatAccuracySeconds());
251        }
252        return true;
253    }
254
255    /**
256     * Find the finest SubGrid containing the coordinate, specified in Positive West Seconds
257     * @param topLevelSubGrid top level subgrid
258     * @param lon Longitude in Positive West Seconds
259     * @param lat Latitude in Seconds
260     * @return The SubGrid found or null
261     */
262    private static NTV2SubGrid getSubGrid(NTV2SubGrid[] topLevelSubGrid, double lon, double lat) {
263        NTV2SubGrid sub = null;
264        for (int i = 0; i < topLevelSubGrid.length; i++) {
265            sub = topLevelSubGrid[i].getSubGridForCoord(lon, lat);
266            if (sub != null) {
267                break;
268            }
269        }
270        return sub;
271    }
272
273    @Override
274    public String toString() {
275        return new StringBuilder(256)
276            .append("Headers  : ")
277            .append(overviewHeaderCount)
278            .append("\nSub Hdrs : ")
279            .append(subGridHeaderCount)
280            .append("\nSub Grids: ")
281            .append(subGridCount)
282            .append("\nType     : ")
283            .append(shiftType)
284            .append("\nVersion  : ")
285            .append(version)
286            .append("\nFr Ellpsd: ")
287            .append(fromEllipsoid)
288            .append("\nTo Ellpsd: ")
289            .append(toEllipsoid)
290            .append("\nFr Maj Ax: ")
291            .append(fromSemiMajorAxis)
292            .append("\nFr Min Ax: ")
293            .append(fromSemiMinorAxis)
294            .append("\nTo Maj Ax: ")
295            .append(toSemiMajorAxis)
296            .append("\nTo Min Ax: ")
297            .append(toSemiMinorAxis)
298            .toString();
299    }
300
301    public String getFromEllipsoid() {
302        return fromEllipsoid;
303    }
304
305    public String getToEllipsoid() {
306        return toEllipsoid;
307    }
308}