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.tools.Logging; 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 algorithms 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 * @since 2507 066 */ 067public class NTV2GridShiftFile implements Serializable { 068 069 private static final long serialVersionUID = 1L; 070 071 private int overviewHeaderCount; 072 private int subGridHeaderCount; 073 private int subGridCount; 074 private String shiftType; 075 private String version; 076 private String fromEllipsoid = ""; 077 private String toEllipsoid = ""; 078 private double fromSemiMajorAxis; 079 private double fromSemiMinorAxis; 080 private double toSemiMajorAxis; 081 private double toSemiMinorAxis; 082 083 private NTV2SubGrid[] topLevelSubGrid; 084 private NTV2SubGrid lastSubGrid; 085 086 private static void readBytes(InputStream in, byte[] b) throws IOException { 087 if (in.read(b) < b.length) { 088 Logging.error("Failed to read expected amount of bytes ("+ b.length +") from stream"); 089 } 090 } 091 092 /** 093 * Load a Grid Shift File from an InputStream. The Grid Shift node 094 * data is stored in Java arrays, which will occupy about the same memory 095 * as the original file with accuracy data included, and about half that 096 * with accuracy data excluded. The size of the Australian national file 097 * is 4.5MB, and the Canadian national file is 13.5MB 098 * <p>The InputStream is closed by this method. 099 * 100 * @param in Grid Shift File InputStream 101 * @param loadAccuracy is Accuracy data to be loaded as well as shift data? 102 * @throws IOException if any I/O error occurs 103 */ 104 public void loadGridShiftFile(InputStream in, boolean loadAccuracy) throws IOException { 105 byte[] b8 = new byte[8]; 106 fromEllipsoid = ""; 107 toEllipsoid = ""; 108 topLevelSubGrid = null; 109 readBytes(in, b8); 110 String overviewHeaderCountId = new String(b8, StandardCharsets.UTF_8); 111 if (!"NUM_OREC".equals(overviewHeaderCountId)) 112 throw new IllegalArgumentException("Input file is not an NTv2 grid shift file"); 113 boolean bigEndian; 114 readBytes(in, b8); 115 overviewHeaderCount = NTV2Util.getIntBE(b8, 0); 116 if (overviewHeaderCount == 11) { 117 bigEndian = true; 118 } else { 119 overviewHeaderCount = NTV2Util.getIntLE(b8, 0); 120 if (overviewHeaderCount == 11) { 121 bigEndian = false; 122 } else 123 throw new IllegalArgumentException("Input file is not an NTv2 grid shift file"); 124 } 125 readBytes(in, b8); 126 readBytes(in, b8); 127 subGridHeaderCount = NTV2Util.getInt(b8, bigEndian); 128 readBytes(in, b8); 129 readBytes(in, b8); 130 subGridCount = NTV2Util.getInt(b8, bigEndian); 131 NTV2SubGrid[] subGrid = new NTV2SubGrid[subGridCount]; 132 readBytes(in, b8); 133 readBytes(in, b8); 134 shiftType = new String(b8, StandardCharsets.UTF_8); 135 readBytes(in, b8); 136 readBytes(in, b8); 137 version = new String(b8, StandardCharsets.UTF_8); 138 readBytes(in, b8); 139 readBytes(in, b8); 140 fromEllipsoid = new String(b8, StandardCharsets.UTF_8); 141 readBytes(in, b8); 142 readBytes(in, b8); 143 toEllipsoid = new String(b8, StandardCharsets.UTF_8); 144 readBytes(in, b8); 145 readBytes(in, b8); 146 fromSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian); 147 readBytes(in, b8); 148 readBytes(in, b8); 149 fromSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian); 150 readBytes(in, b8); 151 readBytes(in, b8); 152 toSemiMajorAxis = NTV2Util.getDouble(b8, bigEndian); 153 readBytes(in, b8); 154 readBytes(in, b8); 155 toSemiMinorAxis = NTV2Util.getDouble(b8, bigEndian); 156 157 for (int i = 0; i < subGridCount; i++) { 158 subGrid[i] = new NTV2SubGrid(in, bigEndian, loadAccuracy); 159 } 160 topLevelSubGrid = createSubGridTree(subGrid); 161 lastSubGrid = topLevelSubGrid[0]; 162 } 163 164 /** 165 * Create a tree of Sub Grids by adding each Sub Grid to its parent (where 166 * it has one), and returning an array of the top level Sub Grids 167 * @param subGrid an array of all Sub Grids 168 * @return an array of top level Sub Grids with lower level Sub Grids set. 169 */ 170 private static NTV2SubGrid[] createSubGridTree(NTV2SubGrid... subGrid) { 171 int topLevelCount = 0; 172 Map<String, List<NTV2SubGrid>> subGridMap = new HashMap<>(); 173 for (int i = 0; i < subGrid.length; i++) { 174 if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) { 175 topLevelCount++; 176 } 177 subGridMap.put(subGrid[i].getSubGridName(), new ArrayList<NTV2SubGrid>()); 178 } 179 NTV2SubGrid[] topLevelSubGrid = new NTV2SubGrid[topLevelCount]; 180 topLevelCount = 0; 181 for (int i = 0; i < subGrid.length; i++) { 182 if ("NONE".equalsIgnoreCase(subGrid[i].getParentSubGridName())) { 183 topLevelSubGrid[topLevelCount++] = subGrid[i]; 184 } else { 185 List<NTV2SubGrid> parent = subGridMap.get(subGrid[i].getParentSubGridName()); 186 parent.add(subGrid[i]); 187 } 188 } 189 NTV2SubGrid[] nullArray = new NTV2SubGrid[0]; 190 for (int i = 0; i < subGrid.length; i++) { 191 List<NTV2SubGrid> subSubGrids = subGridMap.get(subGrid[i].getSubGridName()); 192 if (!subSubGrids.isEmpty()) { 193 NTV2SubGrid[] subGridArray = subSubGrids.toArray(nullArray); 194 subGrid[i].setSubGridArray(subGridArray); 195 } 196 } 197 return topLevelSubGrid; 198 } 199 200 /** 201 * Shift a coordinate in the Forward direction of the Grid Shift File. 202 * 203 * @param gs A GridShift object containing the coordinate to shift 204 * @return True if the coordinate is within a Sub Grid, false if not 205 */ 206 public boolean gridShiftForward(NTV2GridShift gs) { 207 NTV2SubGrid subGrid = null; 208 if (lastSubGrid != null) { 209 // Try the last sub grid first, big chance the coord is still within it 210 subGrid = lastSubGrid.getSubGridForCoord(gs.getLonPositiveWestSeconds(), gs.getLatSeconds()); 211 } 212 if (subGrid == null) { 213 subGrid = getSubGrid(topLevelSubGrid, gs.getLonPositiveWestSeconds(), gs.getLatSeconds()); 214 } 215 if (subGrid == null) { 216 return false; 217 } else { 218 subGrid.interpolateGridShift(gs); 219 gs.setSubGridName(subGrid.getSubGridName()); 220 lastSubGrid = subGrid; 221 return true; 222 } 223 } 224 225 /** 226 * Shift a coordinate in the Reverse direction of the Grid Shift File. 227 * 228 * @param gs A GridShift object containing the coordinate to shift 229 * @return True if the coordinate is within a Sub Grid, false if not 230 */ 231 public boolean gridShiftReverse(NTV2GridShift gs) { 232 // set up the first estimate 233 NTV2GridShift forwardGs = new NTV2GridShift(); 234 forwardGs.setLonPositiveWestSeconds(gs.getLonPositiveWestSeconds()); 235 forwardGs.setLatSeconds(gs.getLatSeconds()); 236 for (int i = 0; i < 4; i++) { 237 if (!gridShiftForward(forwardGs)) 238 return false; 239 forwardGs.setLonPositiveWestSeconds( 240 gs.getLonPositiveWestSeconds() - forwardGs.getLonShiftPositiveWestSeconds()); 241 forwardGs.setLatSeconds(gs.getLatSeconds() - forwardGs.getLatShiftSeconds()); 242 } 243 gs.setLonShiftPositiveWestSeconds(-forwardGs.getLonShiftPositiveWestSeconds()); 244 gs.setLatShiftSeconds(-forwardGs.getLatShiftSeconds()); 245 gs.setLonAccuracyAvailable(forwardGs.isLonAccuracyAvailable()); 246 if (forwardGs.isLonAccuracyAvailable()) { 247 gs.setLonAccuracySeconds(forwardGs.getLonAccuracySeconds()); 248 } 249 gs.setLatAccuracyAvailable(forwardGs.isLatAccuracyAvailable()); 250 if (forwardGs.isLatAccuracyAvailable()) { 251 gs.setLatAccuracySeconds(forwardGs.getLatAccuracySeconds()); 252 } 253 return true; 254 } 255 256 /** 257 * Find the finest SubGrid containing the coordinate, specified in Positive West Seconds 258 * @param topLevelSubGrid top level subgrid 259 * @param lon Longitude in Positive West Seconds 260 * @param lat Latitude in Seconds 261 * @return The SubGrid found or null 262 */ 263 private static NTV2SubGrid getSubGrid(NTV2SubGrid[] topLevelSubGrid, double lon, double lat) { 264 NTV2SubGrid sub = null; 265 for (int i = 0; i < topLevelSubGrid.length; i++) { 266 sub = topLevelSubGrid[i].getSubGridForCoord(lon, lat); 267 if (sub != null) { 268 break; 269 } 270 } 271 return sub; 272 } 273 274 @Override 275 public String toString() { 276 return new StringBuilder(256) 277 .append("Headers : ") 278 .append(overviewHeaderCount) 279 .append("\nSub Hdrs : ") 280 .append(subGridHeaderCount) 281 .append("\nSub Grids: ") 282 .append(subGridCount) 283 .append("\nType : ") 284 .append(shiftType) 285 .append("\nVersion : ") 286 .append(version) 287 .append("\nFr Ellpsd: ") 288 .append(fromEllipsoid) 289 .append("\nTo Ellpsd: ") 290 .append(toEllipsoid) 291 .append("\nFr Maj Ax: ") 292 .append(fromSemiMajorAxis) 293 .append("\nFr Min Ax: ") 294 .append(fromSemiMinorAxis) 295 .append("\nTo Maj Ax: ") 296 .append(toSemiMajorAxis) 297 .append("\nTo Min Ax: ") 298 .append(toSemiMinorAxis) 299 .toString(); 300 } 301 302 /** 303 * Returns "from" ellipsoid identifier. 304 * @return "from" ellipsoid identifier 305 */ 306 public String getFromEllipsoid() { 307 return fromEllipsoid; 308 } 309 310 /** 311 * Returns "to" ellipsoid identifier. 312 * @return "to" ellipsoid identifier 313 */ 314 public String getToEllipsoid() { 315 return toEllipsoid; 316 } 317}