/*
* Copyright (c) 2003 Objectix Pty Ltd All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL OBJECTIX PTY LTD BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openstreetmap.josm.data.projection.datum;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.Utils;
/**
* Models the NTv2 Sub Grid within a Grid Shift File
*
* @author Peter Yuill
* Modified for JOSM :
* - removed the RandomAccessFile mode (Pieren)
* - read grid file by single bytes. Workaround for a bug in some VM not supporting
* file reading by group of 4 bytes from a jar file.
* - removed the Cloneable interface
*/
public class NTV2SubGrid implements Serializable {
private static final long serialVersionUID = 1L;
private final String subGridName;
private final String parentSubGridName;
private final String created;
private final String updated;
private final double minLat;
private final double maxLat;
private final double minLon;
private final double maxLon;
private final double latInterval;
private final double lonInterval;
private final int nodeCount;
private final int lonColumnCount;
private final int latRowCount;
private final float[] latShift;
private final float[] lonShift;
private float[] latAccuracy;
private float[] lonAccuracy;
private NTV2SubGrid[] subGrid;
/**
* Construct a Sub Grid from an InputStream, loading the node data into
* arrays in this object.
*
* @param in GridShiftFile InputStream
* @param bigEndian is the file bigEndian?
* @param loadAccuracy is the node Accuracy data to be loaded?
* @throws IOException if any I/O error occurs
*/
public NTV2SubGrid(InputStream in, boolean bigEndian, boolean loadAccuracy) throws IOException {
byte[] b8 = new byte[8];
byte[] b4 = new byte[4];
byte[] b1 = new byte[1];
readBytes(in, b8);
readBytes(in, b8);
subGridName = new String(b8, StandardCharsets.UTF_8).trim();
readBytes(in, b8);
readBytes(in, b8);
parentSubGridName = new String(b8, StandardCharsets.UTF_8).trim();
readBytes(in, b8);
readBytes(in, b8);
created = new String(b8, StandardCharsets.UTF_8);
readBytes(in, b8);
readBytes(in, b8);
updated = new String(b8, StandardCharsets.UTF_8);
readBytes(in, b8);
readBytes(in, b8);
minLat = NTV2Util.getDouble(b8, bigEndian);
readBytes(in, b8);
readBytes(in, b8);
maxLat = NTV2Util.getDouble(b8, bigEndian);
readBytes(in, b8);
readBytes(in, b8);
minLon = NTV2Util.getDouble(b8, bigEndian);
readBytes(in, b8);
readBytes(in, b8);
maxLon = NTV2Util.getDouble(b8, bigEndian);
readBytes(in, b8);
readBytes(in, b8);
latInterval = NTV2Util.getDouble(b8, bigEndian);
readBytes(in, b8);
readBytes(in, b8);
lonInterval = NTV2Util.getDouble(b8, bigEndian);
lonColumnCount = 1 + (int) ((maxLon - minLon) / lonInterval);
latRowCount = 1 + (int) ((maxLat - minLat) / latInterval);
readBytes(in, b8);
readBytes(in, b8);
nodeCount = NTV2Util.getInt(b8, bigEndian);
if (nodeCount != lonColumnCount * latRowCount)
throw new IllegalStateException("SubGrid " + subGridName + " has inconsistent grid dimesions");
latShift = new float[nodeCount];
lonShift = new float[nodeCount];
if (loadAccuracy) {
latAccuracy = new float[nodeCount];
lonAccuracy = new float[nodeCount];
}
for (int i = 0; i < nodeCount; i++) {
// Read the grid file byte after byte. This is a workaround about a bug in
// certain VM which are not able to read byte blocks when the resource file is in a .jar file (Pieren)
readBytes(in, b1); b4[0] = b1[0];
readBytes(in, b1); b4[1] = b1[0];
readBytes(in, b1); b4[2] = b1[0];
readBytes(in, b1); b4[3] = b1[0];
latShift[i] = NTV2Util.getFloat(b4, bigEndian);
readBytes(in, b1); b4[0] = b1[0];
readBytes(in, b1); b4[1] = b1[0];
readBytes(in, b1); b4[2] = b1[0];
readBytes(in, b1); b4[3] = b1[0];
lonShift[i] = NTV2Util.getFloat(b4, bigEndian);
readBytes(in, b1); b4[0] = b1[0];
readBytes(in, b1); b4[1] = b1[0];
readBytes(in, b1); b4[2] = b1[0];
readBytes(in, b1); b4[3] = b1[0];
if (loadAccuracy) {
latAccuracy[i] = NTV2Util.getFloat(b4, bigEndian);
}
readBytes(in, b1); b4[0] = b1[0];
readBytes(in, b1); b4[1] = b1[0];
readBytes(in, b1); b4[2] = b1[0];
readBytes(in, b1); b4[3] = b1[0];
if (loadAccuracy) {
lonAccuracy[i] = NTV2Util.getFloat(b4, bigEndian);
}
}
}
private static void readBytes(InputStream in, byte[] b) throws IOException {
if (in.read(b) < b.length) {
Main.error("Failed to read expected amount of bytes ("+ b.length +") from stream");
}
}
/**
* Tests if a specified coordinate is within this Sub Grid
* or one of its Sub Grids. If the coordinate is outside
* this Sub Grid, null is returned. If the coordinate is
* within this Sub Grid, but not within any of its Sub Grids,
* this Sub Grid is returned. If the coordinate is within
* one of this Sub Grid's Sub Grids, the method is called
* recursively on the child Sub Grid.
*
* @param lon Longitude in Positive West Seconds
* @param lat Latitude in Seconds
* @return the Sub Grid containing the Coordinate or null
*/
public NTV2SubGrid getSubGridForCoord(double lon, double lat) {
if (isCoordWithin(lon, lat)) {
if (subGrid == null)
return this;
else {
for (NTV2SubGrid aSubGrid : subGrid) {
if (aSubGrid.isCoordWithin(lon, lat))
return aSubGrid.getSubGridForCoord(lon, lat);
}
return this;
}
} else
return null;
}
/**
* Tests if a specified coordinate is within this Sub Grid.
* A coordinate on either outer edge (maximum Latitude or
* maximum Longitude) is deemed to be outside the grid.
*
* @param lon Longitude in Positive West Seconds
* @param lat Latitude in Seconds
* @return true or false
*/
private boolean isCoordWithin(double lon, double lat) {
return (lon >= minLon) && (lon < maxLon) && (lat >= minLat) && (lat < maxLat);
}
/**
* Bi-Linear interpolation of four nearest node values as described in
* 'GDAit Software Architecture Manual' produced by the <a
* href='http://www.dtpli.vic.gov.au/property-and-land-titles/geodesy/geocentric-datum-of-australia-1994-gda94/gda94-useful-tools'>
* Geomatics Department of the University of Melbourne</a>
* @param a value at the A node
* @param b value at the B node
* @param c value at the C node
* @param d value at the D node
* @param x Longitude factor
* @param y Latitude factor
* @return interpolated value
*/
private static double interpolate(float a, float b, float c, float d, double x, double y) {
return a + (((double) b - (double) a) * x) + (((double) c - (double) a) * y) +
(((double) a + (double) d - b - c) * x * y);
}
/**
* Interpolate shift and accuracy values for a coordinate in the 'from' datum
* of the GridShiftFile. The algorithm is described in
* 'GDAit Software Architecture Manual' produced by the <a
* href='http://www.dtpli.vic.gov.au/property-and-land-titles/geodesy/geocentric-datum-of-australia-1994-gda94/gda94-useful-tools'>
* Geomatics Department of the University of Melbourne</a>
* <p>This method is thread safe for both memory based and file based node data.
* @param gs GridShift object containing the coordinate to shift and the shift values
*/
public void interpolateGridShift(NTV2GridShift gs) {
int lonIndex = (int) ((gs.getLonPositiveWestSeconds() - minLon) / lonInterval);
int latIndex = (int) ((gs.getLatSeconds() - minLat) / latInterval);
double x = (gs.getLonPositiveWestSeconds() - (minLon + (lonInterval * lonIndex))) / lonInterval;
double y = (gs.getLatSeconds() - (minLat + (latInterval * latIndex))) / latInterval;
// Find the nodes at the four corners of the cell
int indexA = lonIndex + (latIndex * lonColumnCount);
int indexB = indexA + 1;
int indexC = indexA + lonColumnCount;
int indexD = indexC + 1;
gs.setLonShiftPositiveWestSeconds(interpolate(
lonShift[indexA], lonShift[indexB], lonShift[indexC], lonShift[indexD], x, y));
gs.setLatShiftSeconds(interpolate(
latShift[indexA], latShift[indexB], latShift[indexC], latShift[indexD], x, y));
if (lonAccuracy == null) {
gs.setLonAccuracyAvailable(false);
} else {
gs.setLonAccuracyAvailable(true);
gs.setLonAccuracySeconds(interpolate(
lonAccuracy[indexA], lonAccuracy[indexB], lonAccuracy[indexC], lonAccuracy[indexD], x, y));
}
if (latAccuracy == null) {
gs.setLatAccuracyAvailable(false);
} else {
gs.setLatAccuracyAvailable(true);
gs.setLatAccuracySeconds(interpolate(
latAccuracy[indexA], latAccuracy[indexB], latAccuracy[indexC], latAccuracy[indexD], x, y));
}
}
public String getParentSubGridName() {
return parentSubGridName;
}
public String getSubGridName() {
return subGridName;
}
public int getNodeCount() {
return nodeCount;
}
public int getSubGridCount() {
return subGrid == null ? 0 : subGrid.length;
}
/**
* Set an array of Sub Grids of this sub grid
* @param subGrid subgrids
*/
public void setSubGridArray(NTV2SubGrid... subGrid) {
this.subGrid = Utils.copyArray(subGrid);
}
@Override
public String toString() {
return subGridName;
}
/**
* Returns textual details about the sub grid.
* @return textual details about the sub grid
*/
public String getDetails() {
return new StringBuilder(256)
.append("Sub Grid : ")
.append(subGridName)
.append("\nParent : ")
.append(parentSubGridName)
.append("\nCreated : ")
.append(created)
.append("\nUpdated : ")
.append(updated)
.append("\nMin Lat : ")
.append(minLat)
.append("\nMax Lat : ")
.append(maxLat)
.append("\nMin Lon : ")
.append(minLon)
.append("\nMax Lon : ")
.append(maxLon)
.append("\nLat Intvl: ")
.append(latInterval)
.append("\nLon Intvl: ")
.append(lonInterval)
.append("\nNode Cnt : ")
.append(nodeCount)
.toString();
}
/**
* Get maximum latitude value
* @return maximum latitude
*/
public double getMaxLat() {
return maxLat;
}
/**
* Get maximum longitude value
* @return maximum longitude
*/
public double getMaxLon() {
return maxLon;
}
/**
* Get minimum latitude value
* @return minimum latitude
*/
public double getMinLat() {
return minLat;
}
/**
* Get minimum longitude value
* @return minimum longitude
*/
public double getMinLon() {
return minLon;
}
}