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