blob: 67e982360f0cdd6d8e7063c0c4b62ef102bbdf22 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.referencing.provider;
import java.util.AbstractMap;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.channels.ReadableByteChannel;
import javax.xml.bind.annotation.XmlTransient;
import javax.measure.quantity.Angle;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.Transformation;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.system.DataDirectory;
import org.apache.sis.measure.Units;
/**
* The provider for <cite>"North American Datum Conversion"</cite> (EPSG:9613).
* This transform requires data that are not bundled by default with Apache SIS.
*
* <p>The files given in parameters are theoretically binary files. However this provider accepts also ASCII files.
* Those two kinds of files are recognized automatically; there is no need to specify whether the files are ASCII
* or binary.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @author Rueben Schulz (UBC)
* @version 0.8
*
* @see <a href="http://www.ngs.noaa.gov/cgi-bin/nadcon.prl">NADCON on-line computation</a>
*
* @since 0.7
* @module
*/
@XmlTransient
public final class NADCON extends AbstractProvider {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -4707304160205218546L;
/**
* The operation parameter descriptor for the <cite>"Latitude difference file"</cite> parameter value.
* The default value is {@code "conus.las"}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Latitude difference file </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Default value: {@code conus.las}</li>
* </ul>
*/
private static final ParameterDescriptor<Path> LATITUDE;
/**
* The operation parameter descriptor for the <cite>"Longitude difference file"</cite> parameter value.
* The default value is {@code "conus.los"}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Longitude difference file </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Default value: {@code conus.los}</li>
* </ul>
*/
private static final ParameterDescriptor<Path> LONGITUDE;
/**
* The group of all parameters expected by this coordinate operation.
*/
public static final ParameterDescriptorGroup PARAMETERS;
static {
final ParameterBuilder builder = builder();
LATITUDE = builder
.addIdentifier("8657")
.addName("Latitude difference file")
.create(Path.class, Paths.get("conus.las"));
LONGITUDE = builder
.addIdentifier("8658")
.addName("Longitude difference file")
.create(Path.class, Paths.get("conus.los"));
PARAMETERS = builder
.addIdentifier("9613")
.addName("NADCON")
.createGroup(LATITUDE, LONGITUDE);
}
/**
* Creates a new provider.
*/
public NADCON() {
super(2, 2, PARAMETERS);
}
/**
* Returns the base interface of the {@code CoordinateOperation} instances that use this method.
*
* @return fixed to {@link Transformation}.
*/
@Override
public Class<Transformation> getOperationType() {
return Transformation.class;
}
/**
* Creates a transform from the specified group of parameter values.
*
* @param factory the factory to use if this constructor needs to create other math transforms.
* @param values the group of parameter values.
* @return the created math transform.
* @throws ParameterNotFoundException if a required parameter was not found.
* @throws FactoryException if an error occurred while loading the grid.
*/
@Override
public MathTransform createMathTransform(final MathTransformFactory factory, final ParameterValueGroup values)
throws ParameterNotFoundException, FactoryException
{
final Parameters pg = Parameters.castOrWrap(values);
return InterpolatedTransform.createGeodeticTransformation(factory,
getOrLoad(pg.getMandatoryValue(LATITUDE), pg.getMandatoryValue(LONGITUDE)));
}
/**
* Returns the grid of the given name. This method returns the cached instance if it still exists,
* or load the grid otherwise.
*
* @param latitudeShifts name of the grid file for latitude shifts.
* @param longitudeShifts name of the grid file for longitude shifts.
*/
@SuppressWarnings("null")
static DatumShiftGridFile<Angle,Angle> getOrLoad(final Path latitudeShifts, final Path longitudeShifts)
throws FactoryException
{
final Path rlat = DataDirectory.DATUM_CHANGES.resolve(latitudeShifts).toAbsolutePath();
final Path rlon = DataDirectory.DATUM_CHANGES.resolve(longitudeShifts).toAbsolutePath();
final Object key = new AbstractMap.SimpleImmutableEntry<>(rlat, rlon);
DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(key);
if (grid == null) {
final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(key);
try {
grid = handler.peek();
if (grid == null) {
final Loader loader;
Path file = latitudeShifts;
try {
// Note: buffer size must be divisible by the size of 'float' data type.
final ByteBuffer buffer = ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
final FloatBuffer fb = buffer.asFloatBuffer();
try (ReadableByteChannel in = Files.newByteChannel(rlat)) {
DatumShiftGridLoader.log(NADCON.class, CharSequences.commonPrefix(
latitudeShifts.toString(), longitudeShifts.toString()).toString() + '…');
loader = new Loader(in, buffer, file);
loader.readGrid(fb, null, longitudeShifts);
}
buffer.clear();
file = longitudeShifts;
try (ReadableByteChannel in = Files.newByteChannel(rlon)) {
new Loader(in, buffer, file).readGrid(fb, loader, null);
}
} catch (IOException | NoninvertibleTransformException | RuntimeException e) {
throw DatumShiftGridLoader.canNotLoad("NADCON", file, e);
}
grid = DatumShiftGridCompressed.compress(loader.grid, null, loader.grid.accuracy);
grid = grid.useSharedData();
}
} finally {
handler.putAndUnlock(grid);
}
}
return grid.castTo(Angle.class, Angle.class);
}
/**
* Loaders of NADCON data. Instances of this class exist only at loading time.
* This class can read both binary and ASCII grid files.
*
* <h4>Binary format</h4>
* NADCON binary files ({@code "*.las"} and {@code "*.los"}) are organized into records
* with the first record containing the header information, followed by the shift data.
* The length of each record (including header) depends on the number of columns.
*
* <p>Records are ordered from South to North. Each record (except the header) is an entire row of grid points,
* with values ordered from West to East. Each value is a {@code float} encoded in little endian byte order.
* Each record ends with a 4 byte separator.</p>
*
* <p>Record data use a different convention than the record header. The header uses degrees of angle with
* positive values East. But the offset values after the header are in seconds of angle with positive values
* West. The {@code DatumShiftGrid} returned by this loader uses the header convention, which also matches
* the order in which offset values appear in each row.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @author Rueben Schulz (UBC)
* @version 0.7
* @since 0.7
* @module
*/
private static final class Loader extends DatumShiftGridLoader {
/**
* The length of the description in the header, in bytes.
* <ul>
* <li>56 bytes of NADCON's identification: {@code "NADCON EXTRACTED REGION"}.</li>
* <li>8 bytes of "PGM" (unknown meaning).</li>
* </ul>
*/
private static final int DESCRIPTION_LENGTH = 64;
/**
* The first bytes that we expect to find the header (both binary and ASCII files).
* We use that as the format signature.
*/
private static final String NADCON = "NADCON";
/**
* Longitude and latitude of of the first value in the first record, in degrees.
*/
private final float x0, y0;
/**
* The difference between longitude (<var>Δx</var>) and latitude (<var>Δy</var>) cells, in degrees.
*/
private final float Δx, Δy;
/**
* Number of longitude (<var>nx</var>) and latitude (<var>ny</var>) values.
*/
private final int nx, ny, nz;
/**
* Temporary buffer for ASCII characters, or {@code null} if the file is a binary file.
*/
private final StringBuilder ascii;
/**
* The grid created by {@link #readGrid(FloatBuffer, Loader, Path)}.
*/
DatumShiftGridFile.Float<Angle,Angle> grid;
/**
* Creates a new reader for the given channel. The file can be binary or ASCII.
* This constructor parses the header immediately, but does not read any grid.
*
* @param channel where to read data from.
* @param buffer the buffer to use. That buffer must use little endian byte order
* and have a capacity divisible by the size of the {@code float} type.
* @param file path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
*/
Loader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file)
throws IOException, FactoryException
{
super(channel, buffer, file);
// After the description we need (3 int + 5 float) = 32 bytes
// for a binary header, or 74 characters for an ASCII header.
ensureBufferContains(DESCRIPTION_LENGTH + 80);
for (int i=0; i<NADCON.length(); i++) {
if (buffer.get() != NADCON.charAt(i)) {
throw unexpectedFormat();
}
}
if (isASCII(buffer)) {
ascii = new StringBuilder();
nx = Integer.parseInt(nextWord());
ny = Integer.parseInt(nextWord());
nz = Integer.parseInt(nextWord());
x0 = Float.parseFloat(nextWord());
Δx = Float.parseFloat(nextWord());
y0 = Float.parseFloat(nextWord());
Δy = Float.parseFloat(nextWord());
Float.parseFloat(nextWord());
} else {
ascii = null;
buffer.position(DESCRIPTION_LENGTH);
nx = buffer.getInt(); // Number of data elements in each record.
ny = buffer.getInt(); // Number of records in the file.
nz = buffer.getInt(); // Not used.
x0 = buffer.getFloat(); // Longitude of first record (westmost).
Δx = buffer.getFloat(); // Cell size in degrees of longitude.
y0 = buffer.getFloat(); // Latitude of first record (southmost).
Δy = buffer.getFloat(); // Cell size in degrees of latitude.
// One more float at this position, which we ignore.
}
if (nx < 8 || ny < 1 || nz < 1 || !(Δx > 0) || !(Δy > 0) || Float.isNaN(x0) || Float.isNaN(y0)) {
throw unexpectedFormat();
}
if (ascii == null) {
skip((nx + 1) * Float.BYTES - buffer.position());
}
}
/**
* Returns {@code true} if all remaining characters in the buffer are US-ASCII characters for real numbers,
* except the characters on the first line which may be any printable US-ASCII characters. If the content is
* assumed ASCII, then the buffer position is set to the first EOL character.
*
* @return {@code true} if the file seems to be an ASCII one.
*/
private static boolean isASCII(final ByteBuffer buffer) {
int newLine = 0;
while (buffer.hasRemaining()) {
final char c = (char) buffer.get();
if (c != ' ' && !(c >= '+' && c <= '9' && c != ',' && c != '/')) { // (space) + - . [0-9]
if (c == '\r' || c == '\n') {
if (newLine == 0) {
newLine = buffer.position();
}
} else {
if (newLine == 0 && c >= 32 & c <= 126) {
continue; // Accept other US-ASCII characters ony on the first line.
}
return false;
}
}
}
if (newLine == 0) {
return false; // If it was an ASCII file, we would have found at least one EOL character.
}
buffer.position(newLine);
return true;
}
/**
* Returns the next word from an ASCII file. This method is invoked only for parsing ASCII files,
* in which case the {@link #ascii} string builder is non-null.
*/
private String nextWord() throws IOException {
char c;
do {
ensureBufferContains(1);
c = (char) buffer.get();
} while (Character.isWhitespace(c));
ascii.setLength(0);
do {
ascii.append(c);
ensureBufferContains(1);
c = (char) buffer.get();
} while (!Character.isWhitespace(c));
return ascii.toString();
}
/**
* Returns the exception to thrown in the file does not seems to be a NADCON format.
*/
private FactoryException unexpectedFormat() {
return new FactoryException(Errors.format(Errors.Keys.UnexpectedFileFormat_2, NADCON, file));
}
/**
* Loads latitude or longitude shifts data. This method should be invoked twice:
*
* <ol>
* <li>On an instance created for the latitude shifts file with a {@code latitude} argument set to null.</li>
* <li>On an instance created for the longitude shifts file with a {@code latitude} argument set to the
* instance created in the previous step.</li>
* </ol>
*
* The result is stored in the {@link #grid} field.
*
* @param fb a {@code FloatBuffer} view over the full {@link #buffer} range.
* @param latitudeShifts the previously loaded latitude shifts, or {@code null} if not yet loaded.
* @param longitudeShifts the file for the longitude grid.
*/
final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final Path longitudeShifts)
throws IOException, FactoryException, NoninvertibleTransformException
{
final int dim;
final double scale;
if (latitudeShifts == null) {
dim = 1; // Dimension of latitudes.
scale = DEGREES_TO_SECONDS * Δy; // NADCON shifts are positive north.
grid = new DatumShiftGridFile.Float<>(2, Units.DEGREE, Units.DEGREE,
true, x0, y0, Δx, Δy, nx, ny, PARAMETERS, file, longitudeShifts);
grid.accuracy = SECOND_PRECISION / DEGREES_TO_SECONDS;
} else {
if (x0 != latitudeShifts.x0 || Δx != latitudeShiftsx || nx != latitudeShifts.nx ||
y0 != latitudeShifts.y0 || Δy != latitudeShiftsy || ny != latitudeShifts.ny || nz != latitudeShifts.nz)
{
throw new FactoryException(Errors.format(Errors.Keys.MismatchedGridGeometry_2,
latitudeShifts.file.getFileName(), file.getFileName()));
}
dim = 0; // Dimension of longitudes
scale = -DEGREES_TO_SECONDS * Δx; // NADCON shifts are positive west.
grid = latitudeShifts.grid; // Continue writing in existing grid.
}
final float[] array = grid.offsets[dim];
if (ascii != null) {
for (int i=0; i<array.length; i++) {
array[i] = (float) (Double.parseDouble(nextWord()) / scale);
}
} else {
/*
* Transfer all data from the FloatBuffer to the float[] array, except one float at the beginning
* of every row which must be skipped. That skipped float value is not a translation value and is
* expected to be always zero.
*/
syncView(fb);
int forCurrentRow = 0;
for (int i=0; i<array.length;) {
if (forCurrentRow == 0) {
if (!fb.hasRemaining()) {
fillBuffer(fb);
}
if (fb.get() != 0) {
throw unexpectedFormat();
}
forCurrentRow = nx;
}
int remaining = fb.remaining();
if (remaining == 0) {
fillBuffer(fb);
remaining = fb.remaining();
}
final int n = Math.min(forCurrentRow, remaining);
fb.get(array, i, n);
forCurrentRow -= n;
i += n;
}
/*
* Convert seconds to degrees for consistency with the unit declared at the beginning of this method,
* then divide by cell size for consistency with the 'isCellRatio = true' configuration.
*/
for (int i=0; i<array.length; i++) {
array[i] /= scale;
}
}
}
/**
* Invoked when the given {@code FloatBuffer} buffer is empty. This method requests one {@code float}
* from the channel, but the channel will usually give us as many data as the buffer can contain.
*/
private void fillBuffer(final FloatBuffer fb) throws IOException {
buffer.position(fb.position() * Float.BYTES).limit(fb.limit() * Float.BYTES);
ensureBufferContains(Float.BYTES); // Require at least one float, but usually get many.
syncView(fb);
}
/**
* Sets the position and limit of the given {@code FloatBuffer} to the same position and limit
* than the underlying {@code ByteBuffer}, converted to units of {@code float} data type.
*/
private void syncView(final FloatBuffer fb) {
if ((buffer.position() % Float.BYTES) != 0) {
buffer.compact(); // For bytes alignment with FloatBuffer.
}
fb.limit(buffer.limit() / Float.BYTES).position(buffer.position() / Float.BYTES);
}
}
}