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}