blob: ebc74eeceb71b399aea32f33d389d4ed8c1001b8 [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.Arrays;
import java.lang.reflect.Array;
import java.nio.file.Path;
import javax.measure.Unit;
import javax.measure.Quantity;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.referencing.datum.DatumShiftGrid;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
/**
* A datum shift grid loaded from a file.
* The filename is usually a parameter defined in the EPSG database.
* This class should not be in public API because it requires implementation to expose internal mechanic:
* Subclasses need to give an access to their internal data (not a copy) through the {@link #getData()}
* and {@link #setData(Object[])} methods. We use that for managing the cache, reducing memory usage by
* sharing data and for {@link #equals(Object)} and {@link #hashCode()} implementations.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @param <C> dimension of the coordinate unit (usually {@link javax.measure.quantity.Angle}).
* @param <T> dimension of the translation unit (usually {@link javax.measure.quantity.Angle}
* or {@link javax.measure.quantity.Length}).
*
* @see org.apache.sis.referencing.operation.transform.InterpolatedTransform
*
* @since 0.7
* @module
*/
abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>> extends DatumShiftGrid<C,T> {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -4471670781277328193L;
/**
* Cache of grids loaded so far. Those grids will be stored by soft references until the amount of
* data exceed 32768 (about 128 kilobytes if the values use the {@code float} type). in which case
* the oldest grids will be replaced by weak references.
*/
static final Cache<Object, DatumShiftGridFile<?,?>> CACHE = new Cache<Object, DatumShiftGridFile<?,?>>(4, 32*1024, true) {
@Override protected int cost(final DatumShiftGridFile<?,?> grid) {
int p = 1;
for (final Object array : grid.getData()) {
p *= Array.getLength(array);
}
return p;
}
};
/**
* The parameter descriptor of the provider that created this grid.
*/
private final ParameterDescriptorGroup descriptor;
/**
* The files from which the grid has been loaded. This is not used directly by this class
* (except for {@link #equals(Object)} and {@link #hashCode()}), but can be used by math
* transform for setting the parameter values. Shall never be null and never empty.
*/
private final Path[] files;
/**
* Number of grid cells along the <var>x</var> axis.
* This is <code>{@linkplain #getGridSize()}[0]</code> as a field for performance reasons.
*/
protected final int nx;
/**
* The best translation accuracy that we can expect from this file.
* The unit of measurement depends on {@link #isCellValueRatio()}.
*
* <p>This field is initialized to {@link Double#NaN}. It is loader responsibility
* to assign a value to this field after {@code DatumShiftGridFile} construction.</p>
*
* @see #getCellPrecision()
*/
double accuracy;
/**
* Creates a new datum shift grid for the given grid geometry.
* The actual offset values need to be provided by subclasses.
*
* @param coordinateUnit the unit of measurement of input values, before conversion to grid indices by {@code coordinateToGrid}.
* @param translationUnit the unit of measurement of output values.
* @param isCellValueRatio {@code true} if results of {@link #interpolateInCell interpolateInCell(…)} are divided by grid cell size.
* @param x0 longitude in degrees of the center of the cell at grid index (0,0).
* @param y0 latitude in degrees of the center of the cell at grid index (0,0).
* @param Δx increment in <var>x</var> value between cells at index <var>gridX</var> and <var>gridX</var> + 1.
* @param Δy increment in <var>y</var> value between cells at index <var>gridY</var> and <var>gridY</var> + 1.
* @param nx number of cells along the <var>x</var> axis in the grid.
* @param ny number of cells along the <var>y</var> axis in the grid.
* @param descriptor the parameter descriptor of the provider that created this grid.
* @param files the file(s) from which the grid has been loaded. This array is not cloned.
*/
DatumShiftGridFile(final Unit<C> coordinateUnit,
final Unit<T> translationUnit,
final boolean isCellValueRatio,
final double x0, final double y0,
final double Δx, final double Δy,
final int nx, final int ny,
final ParameterDescriptorGroup descriptor,
final Path... files) throws NoninvertibleTransformException
{
super(coordinateUnit, new AffineTransform2Dx, 0, 0, Δy, x0, y0).inverse(),
new int[] {nx, ny}, isCellValueRatio, translationUnit);
this.descriptor = descriptor;
this.files = files;
this.nx = nx;
this.accuracy = Double.NaN;
}
/**
* Creates a new datum shift grid with the same grid geometry than the given grid.
*
* @param other the other datum shift grid from which to copy the grid geometry.
*/
protected DatumShiftGridFile(final DatumShiftGridFile<C,T> other) {
super(other);
descriptor = other.descriptor;
files = other.files;
nx = other.nx;
accuracy = other.accuracy;
}
/**
* Returns {@code this} casted to the given type, after verification that those types are valid.
* This method is invoked after {@link NADCON}, {@link NTv2} or other providers got an existing
* {@code DatumShiftGridFile} instance from the {@link #CACHE}.
*/
@SuppressWarnings("unchecked")
final <NC extends Quantity<NC>, NT extends Quantity<NT>> DatumShiftGridFile<NC,NT> castTo(
final Class<NC> coordinateType, final Class<NT> translationType)
{
super.getCoordinateUnit() .asType(coordinateType);
super.getTranslationUnit().asType(translationType);
return (DatumShiftGridFile<NC,NT>) this;
}
/**
* If a grid exists in the cache for the same data, returns a new grid sharing the same data arrays.
* Otherwise returns {@code this}.
*
* @return a grid using the same data than this grid, or {@code this}.
*
* @see #getData()
* @see #setData(Object[])
*/
protected final DatumShiftGridFile<C,T> useSharedData() {
final Object[] data = getData();
for (final DatumShiftGridFile<?,?> grid : CACHE.values()) {
final Object[] other = grid.getData();
if (Arrays.deepEquals(data, other)) {
return setData(other);
}
}
return this;
}
/**
* Returns a new grid with the same geometry than this grid but different data arrays.
* This method is invoked by {@link #useSharedData()} when it detected that a newly created grid uses
* the same data than an existing grid. The typical use case is when a filename is different but still
* reference the same grid (e.g. symbolic link, lower case versus upper case in a case-insensitive file
* system).
*
* @param other data from another {@code DatumShiftGridFile} that we can share.
* @return a new {@code DatumShiftGridFile} using the given data reference.
*/
protected abstract DatumShiftGridFile<C,T> setData(Object[] other);
/**
* Returns the data for each shift dimensions. This method is for cache management, {@link #equals(Object)}
* and {@link #hashCode()} implementations only and should not be invoked in other context.
*
* @return a direct (not cloned) reference to the internal data array.
*/
protected abstract Object[] getData();
/**
* Suggests a precision for the translation values in this grid.
* This information is used for deciding when to stop iterations in inverse transformations.
* The default implementation returns the {@linkplain #accuracy} divided by an arbitrary value.
*
* @return a precision for the translation values in this grid.
*/
@Override
public double getCellPrecision() {
return accuracy / 10; // Division by 10 is arbitrary.
}
/**
* Returns the descriptor specified at construction time.
*
* @return a description of the values in this grid.
*/
@Override
public final ParameterDescriptorGroup getParameterDescriptors() {
return descriptor;
}
/**
* Sets all parameters for a value of type {@link Path} to the values given to the constructor.
* Subclasses may override for defining other kinds of parameters too.
*
* @param parameters the parameter group where to set the values.
*/
@Override
public final void getParameterValues(final Parameters parameters) {
int i = 0;
for (final GeneralParameterDescriptor gd : descriptor.descriptors()) {
if (gd instanceof ParameterDescriptor<?>) {
final ParameterDescriptor<?> d = (ParameterDescriptor<?>) gd;
if (Path.class.isAssignableFrom(d.getValueClass())) {
if (i >= files.length) break; // Safety in case of invalid parameters.
parameters.getOrCreate(d).setValue(files[i++]);
}
}
}
}
/**
* Returns {@code true} if the given object is a grid containing the same data than this grid.
*
* @param other the other object to compare with this datum shift grid.
* @return {@code true} if the given object is non-null, of the same class than this {@code DatumShiftGrid}
* and contains the same data.
*/
@Override
public boolean equals(final Object other) {
if (other == this) { // Optimization for a common case.
return true;
}
if (super.equals(other)) {
final DatumShiftGridFile<?,?> that = (DatumShiftGridFile<?,?>) other;
return Arrays.equals(files, that.files) && Arrays.deepEquals(getData(), that.getData());
}
return false;
}
/**
* Returns a hash code value for this datum shift grid.
*
* @return {@inheritDoc}
*/
@Override
public int hashCode() {
return super.hashCode() + Arrays.hashCode(files);
}
/**
* An implementation of {@link DatumShiftGridFile} which stores the offset values in {@code float[]} arrays.
* This class is in internal package (not public API) because it makes the following assumptions:
* <ul>
* <li>Values <var>x₀</var>, <var>y₀</var>, <var>Δx</var> and <var>Δy</var>
* given to the constructor are in degrees and needs to be converted to radians.</li>
* <li>Single floating-point precision ({@code float)} is sufficient.</li>
* <li>Values were defined in base 10, usually in ASCII files. This assumption has an impact on conversions
* from {@code float} to {@code double} performed by the {@link #getCellValue(int, int, int)} method.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.7
* @since 0.7
* @module
*/
static final class Float<C extends Quantity<C>, T extends Quantity<T>> extends DatumShiftGridFile<C,T> {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -9221609983475286496L;
/**
* The translation values. {@code offsets.length} is the number of dimensions, and {@code offsets[dim].length}
* shall be the same for all {@code dim} value. Component {@code dim} of the translation vector at coordinate
* {@code gridX}, {@code gridY} is {@code offsets[dim][gridX + gridY*nx]}.
*/
final float[][] offsets;
/**
* Creates a new datum shift grid with the given grid geometry, filename and number of shift dimensions.
* All {@code double} values given to this constructor will be converted from degrees to radians.
*/
Float(final int dim,
final Unit<C> coordinateUnit,
final Unit<T> translationUnit,
final boolean isCellValueRatio,
final double x0, final double y0,
final double Δx, final double Δy,
final int nx, final int ny,
final ParameterDescriptorGroup descriptor,
final Path... files) throws NoninvertibleTransformException
{
super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, Δx, Δy, nx, ny, descriptor, files);
offsets = new float[dim][];
final int size = Math.multiplyExact(nx, ny);
for (int i=0; i<dim; i++) {
Arrays.fill(offsets[i] = new float[size], java.lang.Float.NaN);
}
}
/**
* Creates a new grid of the same geometry than the given grid but using a different data array.
*/
private Float(final DatumShiftGridFile<C,T> grid, final float[][] offsets) {
super(grid);
this.offsets = offsets;
}
/**
* Returns a new grid with the same geometry than this grid but different data arrays.
*/
@Override
protected final DatumShiftGridFile<C,T> setData(final Object[] other) {
return new Float<>(this, (float[][]) other);
}
/**
* Returns direct references (not cloned) to the data arrays. This method is for cache management,
* {@link #equals(Object)} and {@link #hashCode()} implementations only and should not be invoked
* in other context.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
protected final Object[] getData() {
return offsets;
}
/**
* Returns the number of shift dimensions.
*/
@Override
public final int getTranslationDimensions() {
return offsets.length;
}
/**
* Returns the cell value at the given dimension and grid index.
* This method casts the {@code float} values to {@code double} by setting the extra <em>decimal</em> digits
* (not the <em>binary</em> digits) to 0. This is on the assumption that the {@code float} values were parsed
* from an ASCII file, or any other medium that format numbers in base 10.
*
* @param dim the dimension for which to get an average value.
* @param gridX the grid index along the <var>x</var> axis, from 0 inclusive to {@link #nx} exclusive.
* @param gridY the grid index along the <var>y</var> axis, from 0 inclusive to {@code ny} exclusive.
* @return the offset at the given dimension in the grid cell at the given index.
*/
@Override
public final double getCellValue(final int dim, final int gridX, final int gridY) {
return DecimalFunctions.floatToDouble(offsets[dim][gridX + gridY*nx]);
}
}
}