blob: ed7bbaf7d0a14962e1d07022b1a8c6d510a4bc37 [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.coverage.grid;
import java.util.Map;
import java.io.Serializable;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.metadata.spatial.PixelOrientation;
import static org.opengis.metadata.spatial.PixelOrientation.*;
import org.apache.sis.util.Static;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.transform.MathTransforms;
/**
* The translation to apply for different values of {@link PixelOrientation} or {@link PixelInCell}.
* The translation are returned by a call to one of the following static methods:
*
* <ul>
* <li>{@link #getPixelTranslation(PixelOrientation)} for the two-dimensional case.</li>
* <li>{@link #getPixelTranslation(PixelInCell)} for the <var>n</var>-dimensional case.</li>
* </ul>
*
* This class provides also a few {@code translate(…)} convenience methods,
* which apply the translation on a given {@link MathTransform} instance.
*
* <h2>Example</h2>
* In the following code snippet, {@code gridToCRS} is an {@link java.awt.geom.AffineTransform} from
* <i>grid cell</i> coordinates (typically pixel coordinates) to some arbitrary CRS coordinates.
* In this example, the transform maps pixels {@linkplain PixelOrientation#CENTER center},
* while the {@linkplain PixelOrientation#UPPER_LEFT upper left} corner is desired.
* This code will switch the affine transform from the <i>pixel center</i> to
* <i>upper left corner</i> convention:
*
* {@snippet lang="java" :
* public AffineTransform getGridToPixelCorner() {
* AffineTransform gridToCRS = ...;
* PixelOrientation current = PixelOrientation.CENTER;
* PixelOrientation desired = PixelOrientation.UPPER_LEFT;
*
* // Switch the transform from 'current' to 'desired' convention.
* PixelTranslation source = getPixelTranslation(current);
* PixelTranslation target = getPixelTranslation(desired);
* return gridToCRS.translate(target.dx - source.dx,
* target.dy - source.dy);
* }
* }
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.5
*
* @see PixelInCell
* @see PixelOrientation
*
* @since 1.0
*/
public final class PixelTranslation extends Static implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -5671620211497720808L;
/**
* Math transforms created by {@link #translate(MathTransform, PixelInCell, PixelInCell)}
* for dimensions 1 to 6. Each element in this array will be created when first needed.
* Even indices are translations by -0.5 while odd indices are translations by +0.5.
*/
private static final MathTransform[] translations = new MathTransform[10];
/**
* The pixel orientation for this translation.
* Most common values are {@link PixelOrientation#UPPER_LEFT} and {@link PixelOrientation#CENTER}.
*/
public final PixelOrientation orientation;
/**
* The translation among the <var>x</var> axis relative to pixel center.
* The value is typically −½, 0 or +½.
*/
public final double dx;
/**
* The translation among the <var>y</var> axis relative to pixel center.
* The value is typically −½, 0 or +½.
*/
public final double dy;
/**
* The offset for various pixel orientations. Keys must be upper-case names.
*/
private static final Map<PixelOrientation, PixelTranslation> ORIENTATIONS = Map.ofEntries(
entry(CENTER, 0.0, 0.0),
entry(UPPER_LEFT, -0.5, -0.5),
entry(UPPER_RIGHT, 0.5, -0.5),
entry(LOWER_LEFT, -0.5, 0.5),
entry(LOWER_RIGHT, 0.5, 0.5));
/** For {@link #ORIENTATIONS} construction only. */
private static Map.Entry<PixelOrientation, PixelTranslation> entry(
PixelOrientation orientation, double dx, double dy)
{
return Map.entry(orientation, new PixelTranslation(orientation, dx, dy));
}
/**
* Creates a new pixel translation.
*/
private PixelTranslation(final PixelOrientation orientation, final double dx, final double dy) {
this.orientation = orientation;
this.dx = dx;
this.dy = dy;
}
/**
* Returns the pixel orientation which is equivalent to the given {@code PixelInCell} code.
* This equivalence can be used for converting <var>n</var>-dimensional parameters to the
* more specific two-dimensional case. This method implements the following mapping:
*
* <table class="sis">
* <caption>Pixel orientation equivalences</caption>
* <tr><th>Pixel in cell</th><th>Pixel orientation</th></tr>
* <tr><td>{@link PixelInCell#CELL_CENTER CELL_CENTER}</td><td>{@link PixelOrientation#CENTER CENTER}</td></tr>
* <tr><td>{@link PixelInCell#CELL_CORNER CELL_CORNER}</td><td>{@link PixelOrientation#UPPER_LEFT UPPER_LEFT}</td></tr>
* <tr><td>{@code null}</td><td>{@code null}</td></tr>
* </table>
*
* @param anchor the {@code PixelInCell} code, or {@code null}.
* @return the corresponding pixel orientation, or {@code null} if the argument was null.
*/
public static PixelOrientation getPixelOrientation(final PixelInCell anchor) {
return (anchor == null) ? null : anchor.orientation;
}
/**
* Returns the position relative to the cell center.
* This method is typically used for <var>n</var>-dimensional grids, where the number of dimension is unknown.
* The translation is determined from the following table, with the same value applied to all dimensions:
*
* <table class="sis">
* <caption>Translations</caption>
* <tr><th>Pixel in cell</th><th>offset</th></tr>
* <tr><td>{@link PixelInCell#CELL_CENTER CELL_CENTER}</td><td>{@code  0.0}</td></tr>
* <tr><td>{@link PixelInCell#CELL_CORNER CELL_CORNER}</td><td>{@code -0.5}</td></tr>
* </table>
*
* @param anchor the "pixel in cell" value.
* @return the translation for the given "pixel in cell" value.
*/
public static double getPixelTranslation(final PixelInCell anchor) {
return anchor.translationFromCentre;
}
/**
* Returns the specified position relative to the pixel center.
* This method can be used for grid restricted to 2 dimensions.
* The translation vector is determined from the following table:
*
* <table class="sis">
* <caption>Translations</caption>
* <tr><th>Pixel orientation</th> <th> dx </th><th> dy </th></tr>
* <tr><td>{@link PixelOrientation#CENTER CENTER}</td> <td>{@code  0.0}</td><td>{@code  0.0}</td></tr>
* <tr><td>{@link PixelOrientation#UPPER_LEFT UPPER_LEFT}</td> <td>{@code -0.5}</td><td>{@code -0.5}</td></tr>
* <tr><td>{@link PixelOrientation#UPPER_RIGHT UPPER_RIGHT}</td><td>{@code +0.5}</td><td>{@code -0.5}</td></tr>
* <tr><td>{@link PixelOrientation#LOWER_LEFT LOWER_LEFT}</td> <td>{@code -0.5}</td><td>{@code +0.5}</td></tr>
* <tr><td>{@link PixelOrientation#LOWER_RIGHT LOWER_RIGHT}</td><td>{@code +0.5}</td><td>{@code +0.5}</td></tr>
* </table>
*
* @param anchor the pixel orientation.
* @return the position relative to the pixel center.
* @throws IllegalArgumentException if the given {@code anchor} is not a known code list value.
*/
public static PixelTranslation getPixelTranslation(final PixelOrientation anchor) {
final PixelTranslation offset = ORIENTATIONS.get(anchor);
if (offset == null) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
}
return offset;
}
/**
* Converts a math transform from a "pixel in cell" convention to another "pixel in cell" convention.
* This method concatenates −½, 0 or +½ translations on <em>all</em> dimensions before the given transform.
* If the two given conventions are the same, then this method returns the given transform unchanged.
*
* <p>If the given {@code gridToCRS} is null, then this method ignores all other arguments and returns {@code null}.
* Otherwise {@code current} and {@code desired} arguments must be non-null.</p>
*
* <h4>Example</h4>
* If a given {@code gridToCRS} transform was mapping the <em>cell corner</em> to "real world" coordinates, then a call to
* <code>translate(gridToCRS, {@link PixelInCell#CELL_CORNER CELL_CORNER}, {@link PixelInCell#CELL_CENTER CELL_CENTER})</code>
* will return a new transform performing the following steps: first convert grid coordinates from <var>cell center</var>
* convention ({@code desired}) to <var>cell corner</var> convention ({@code current}), then concatenate the given
* {@code gridToCRS} transform which was designed for the <em>cell corner</em> convention.
* The above-cited <var>cell center</var> → <var>cell corner</var> conversion is done by translating the grid coordinates
* by +½, because the grid coordinates (0,0) relative to cell center is (½,½) relative to cell corner.
*
* @param gridToCRS a math transform from pixel coordinates to any CRS, or {@code null}.
* @param current the pixel orientation of the given {@code gridToCRS} transform.
* @param desired the pixel orientation of the desired transform.
* @return the translation from {@code current} to {@code desired}, or {@code null} if {@code gridToCRS} was null.
* @throws IllegalArgumentException if {@code current} or {@code desired} is not a known code list value.
*/
public static MathTransform translate(final MathTransform gridToCRS, final PixelInCell current, final PixelInCell desired) {
if (gridToCRS == null || desired == current) {
return gridToCRS;
}
final int dimension = gridToCRS.getSourceDimensions();
final double offset = desired.translationFromCentre - current.translationFromCentre;
final int ci; // Cache index.
if (offset == -0.5) {
ci = 2*dimension - 2;
} else if (offset == 0.5) {
ci = 2*dimension - 1;
} else {
ci = -1;
}
MathTransform mt;
if (ci < 0 || ci >= translations.length) {
mt = MathTransforms.uniformTranslation(dimension, offset);
} else synchronized (translations) {
mt = translations[ci];
if (mt == null) {
mt = MathTransforms.uniformTranslation(dimension, offset);
translations[ci] = mt;
}
}
return MathTransforms.concatenate(mt, gridToCRS);
}
/**
* Converts a math transform from a "pixel orientation" convention to another "pixel orientation" convention.
* This method concatenates −½, 0 or +½ translations on <em>two</em> dimensions before the given transform.
* The given transform can have any number of input and output dimensions, but only two of them will be converted.
*
* <p>If the given {@code gridToCRS} is null, then this method ignores all other arguments and returns {@code null}.
* Otherwise {@code current} and {@code desired} arguments must be non-null.</p>
*
* <h4>Example</h4>
* If a given {@code gridToCRS} transform was mapping the upper-left corner to "real world" coordinates, then a call to
* <code>translate(gridToCRS, {@link PixelOrientation#UPPER_LEFT UPPER_LEFT}, {@link PixelOrientation#CENTER CENTER}, 0, 1)</code>
* will return a new transform translating grid coordinates by +0.5 before to apply the given {@code gridToCRS} transform.
* See example in above {@link #translate(MathTransform, PixelInCell, PixelInCell) translate} method for more details.
*
* @param gridToCRS a math transform from pixel coordinates to any CRS, or {@code null}.
* @param current the pixel orientation of the given {@code gridToCRS} transform.
* @param desired the pixel orientation of the desired transform.
* @param xDimension the dimension of <var>x</var> coordinates (pixel columns). Often 0.
* @param yDimension the dimension of <var>y</var> coordinates (pixel rows). Often 1.
* @return the translation from {@code current} to {@code desired}, or {@code null} if {@code gridToCRS} was null.
* @throws IllegalArgumentException if {@code current} or {@code desired} is not a known code list value.
*/
public static MathTransform translate(final MathTransform gridToCRS,
final PixelOrientation current, final PixelOrientation desired,
final int xDimension, final int yDimension)
{
if (gridToCRS == null || desired.equals(current)) {
return gridToCRS;
}
final int dimension = gridToCRS.getSourceDimensions();
if (xDimension < 0 || xDimension >= dimension) {
throw illegalDimension("xDimension", xDimension);
}
if (yDimension < 0 || yDimension >= dimension) {
throw illegalDimension("yDimension", yDimension);
}
if (xDimension == yDimension) {
throw illegalDimension("xDimension", "yDimension");
}
final PixelTranslation source = getPixelTranslation(current);
final PixelTranslation target = getPixelTranslation(desired);
final double dx = target.dx - source.dx;
final double dy = target.dy - source.dy;
MathTransform mt;
if (dimension == 2 && (xDimension | yDimension) == 1 && dx == dy && Math.abs(dx) == 0.5) {
final int ci = (dx >= 0) ? 3 : 2;
synchronized (translations) {
mt = translations[ci];
if (mt == null) {
mt = MathTransforms.uniformTranslation(dimension, dx);
translations[ci] = mt;
}
}
} else {
final Matrix matrix = Matrices.createIdentity(dimension + 1);
matrix.setElement(xDimension, dimension, dx);
matrix.setElement(yDimension, dimension, dy);
mt = MathTransforms.linear(matrix);
}
return MathTransforms.concatenate(mt, gridToCRS);
}
/**
* Formats an exception for an illegal dimension.
*/
private static IllegalArgumentException illegalDimension(final String name, final Object dimension) {
return new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, name, dimension));
}
/**
* Returns a string representation of this pixel translation.
*/
@Override
public String toString() {
return String.valueOf(orientation) + '[' + dx + ", " + dy + ']';
}
}