blob: fc4990ecd65bf3af46a38e8f8fb367c11afe6920 [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.Arrays;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.builder.LinearTransformBuilder;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.util.ArgumentChecks;
/**
* Method for replacing a non-linear "grid to CRS" conversion by a linear conversion (affine transform).
* The {@code GridGeometry} class allows non-linear {@linkplain GridGeometry#getGridToCRS "grid to CRS"}
* conversions, but some {@code GridGeometry} usages are restricted to linear (affine) conversions.
* The {@code DomainLinearizer} class encapsulates the method used for replacing non-linear conversions
* by a linear approximation.
*
* <p>The same instance can be reused by invoking {@code apply(…)} methods for many {@link GridCoverage}
* or {@link GridGeometry} instances.</p>
*
* <h2>Limitations</h2>
* Current implementation is designed for two-dimensional grid geometries.
* Support for higher dimensions is not guaranteed.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
* @see LinearTransformBuilder#approximate(MathTransform, Envelope)
*
* @since 1.1
*/
public class DomainLinearizer {
/**
* Whether to force lower grid coordinates to (0,0,…).
*
* @see #getGridStartsAtZero()
*/
private boolean gridStartsAtZero;
/**
* Scale factor applied on grid coordinates, or 1 if none.
*
* @see #getScaleFactor()
*/
private double scale = 1;
/**
* The processor to use for coverage ressampling, created when first needed.
*
* @see #processor()
*/
private GridCoverageProcessor processor;
/**
* Creates a new linearizer.
*/
public DomainLinearizer() {
}
/**
* Returns whether {@code GridExtent} should have their lower grid coordinates set to zero.
* If {@code true}, then {@code DomainLinearizer} will opportunistically apply translation
* on the {@linkplain GridGeometry#getGridToCRS "grid to CRS"} conversion in such a way that
* {@link GridExtent#getLow()} is 0 for all dimensions.
*
* @return whether to force lower grid coordinates to (0,0,…).
*
* @see GridExtent#startsAtZero()
*/
public boolean getGridStartsAtZero() {
return gridStartsAtZero;
}
/**
* Sets whether {@code GridExtent} should have their lower grid coordinates set to zero.
* The default value is {@code false}.
*
* @param force whether to force lower grid coordinates to (0,0,…).
*/
public void setGridStartsAtZero(final boolean force) {
gridStartsAtZero = force;
}
/**
* Returns the scale factor applied on coordinates in all dimensions.
*
* @return scale factor applied on coordinates in all dimensions, or 1 if none.
*/
public double getScaleFactor() {
return scale;
}
/**
* Sets the scale factor to apply on coordinates in all dimensions.
* Must be a value greater than zero. The default value is 1.
*
* @param factor scale factor applied on coordinates in all dimensions, or 1 if none.
*/
public void setScaleFactor(final double factor) {
ArgumentChecks.ensureStrictlyPositive("factor", factor);
scale = factor;
}
/**
* Returns the grid coverage processor associated to this linearizer.
*/
private GridCoverageProcessor processor() {
if (processor == null) {
processor = new GridCoverageProcessor();
}
return processor;
}
/**
* Returns a grid coverage with a linear approximation of the <i>grid to CRS</i> conversion.
* The linear approximation is computed by {@link #apply(GridGeometry)}. If the <i>grid to CRS</i>
* conversion of the given coverage is already linear, then this method returns {@code coverage}.
*
* @param coverage the grid coverage in which to make the <i>grid to CRS</i> conversion linear.
* @return a grid coverage with a linear approximation of the <i>grid to CRS</i> conversion.
* @throws TransformException if some cell coordinates cannot be computed.
*/
public GridCoverage apply(final GridCoverage coverage) throws TransformException {
final GridGeometry gg = coverage.getGridGeometry();
final GridGeometry linearized = apply(gg);
if (gg.equals(linearized)) {
return coverage;
}
return processor().resample(coverage, linearized);
}
/**
* Creates a grid geometry with a linear approximation of the <i>grid to CRS</i> conversion.
* The approximation is computed by <i>Least Mean Squares</i> method: the affine transform
* coefficients are chosen in way making the average value of (<var>position</var> − <var>linear
* approximation of position</var>)² as small as possible for all cells in given grid geometry.
* If the <i>grid to CRS</i> conversion of the given grid geometry is already linear,
* then this method returns {@code gg}.
*
* @param gg the grid geometry in which to make the <i>grid to CRS</i> conversion linear.
* @return a grid geometry with a linear approximation of the <i>grid to CRS</i> conversion.
* @throws TransformException if some cell coordinates cannot be computed.
*/
public GridGeometry apply(final GridGeometry gg) throws TransformException {
if (gg.nonLinears != 0) try {
MathTransform gridToCRS = gg.requireGridToCRS(true);
GeneralEnvelope domain = gg.extent.toEnvelope();
MathTransform approximate = modify(LinearTransformBuilder.approximate(gridToCRS, domain));
MathTransform gridToGrid = MathTransforms.concatenate(gridToCRS, approximate.inverse());
domain = Envelopes.transform(gridToGrid, domain);
final int dimension = domain.getDimension();
final long[] coordinates = new long[dimension * 2];
final double[] shift = new double[dimension];
for (int i=0; i<dimension; i++) {
long low = Math.round(domain.getMinimum(i));
long high = Math.round(domain.getMaximum(i));
high = Math.max(low, Math.decrementExact(high));
if (gridStartsAtZero) {
high = Math.subtractExact(high, low);
shift[i] = low;
} else {
coordinates[i] = low;
}
coordinates[i + dimension] = high;
}
approximate = MathTransforms.concatenate(MathTransforms.translation(shift), approximate);
if (!approximate.equals(gridToCRS)) {
return new GridGeometry(new GridExtent(gg.extent, coordinates), PixelInCell.CELL_CENTER,
approximate, gg.envelope.getCoordinateReferenceSystem());
}
} catch (FactoryException e) {
final Throwable cause = e.getCause();
if (cause instanceof TransformException) {
throw (TransformException) cause;
}
throw new TransformException(e);
}
return gg;
}
/**
* Callback for custom modification of linear approximation. This method is invoked by {@link #apply(GridGeometry)}
* after a linear "grid to CRS" approximation has been computed by the <i>Least Mean Squares</i> method.
* Subclasses can override this method for example in order to scale the conversion by some arbitrary factor.
*
* <h4>Tip</h4>
* Scales (if desired) should be applied <em>before</em> {@code gridToCRS}, i.e. on grid coordinates.
* Scales applied after the transform (i.e. on "real world" coordinates) may give unexpected results
* if the conversion contains a rotation.
*
* @param gridToCRS an approximation of the "grid to CRS" conversion computed by {@code DomainLinearizer}.
* @return the approximation to use for creating a new {@link GridGeometry}. Should be linear.
*/
private MathTransform modify(final LinearTransform gridToCRS) {
final double[] factors = new double[gridToCRS.getTargetDimensions()];
Arrays.fill(factors, scale);
return MathTransforms.concatenate(MathTransforms.scale(factors), gridToCRS);
}
}