This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit f9060a68d408380d15cffa4c8387fd78a79080c4 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Sep 5 18:24:55 2022 +0200 Parameters reported by `SingleOperation.getParameters()` sometime did not described what the `MathTransform` was really doing. Now when a correct set of parameters can not be easily determined, the parameter group is left unspecified (null reference). Parameters that can be inferred from the context should not be listed, because they may cause `InvalidParameterNameException` when those values are copied in a `ParameterValueGroup` that do not expect them (e.g. if trying to copy "semi_major" value in a group of parameters defined by EPSG database). --- .../provider/FranceGeocentricInterpolation.java | 3 +- .../GeocentricAffineBetweenGeographic.java | 2 +- .../provider/GeographicToGeocentric.java | 2 +- .../internal/referencing/provider/Molodensky.java | 2 +- .../sis/parameter/AbstractParameterDescriptor.java | 2 +- .../apache/sis/parameter/FilteredParameters.java | 114 +++++++++++ .../java/org/apache/sis/parameter/Parameters.java | 43 +++++ .../parameter/UnmodifiableParameterValueGroup.java | 21 ++- .../operation/AbstractSingleOperation.java | 62 +++++- .../operation/CoordinateOperationFinder.java | 7 +- .../operation/CoordinateOperationRegistry.java | 7 +- .../referencing/operation/DefaultConversion.java | 12 +- .../sis/referencing/operation/package-info.java | 2 +- .../transform/DefaultMathTransformFactory.java | 210 ++++++++++++++++----- .../org/apache/sis/geometry/TransformTestCase.java | 7 +- .../org/apache/sis/internal/util/Constants.java | 9 +- 16 files changed, 433 insertions(+), 72 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java index 8fab5d5c84..3e3ba90fdb 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java @@ -60,6 +60,7 @@ import org.apache.sis.referencing.datum.DefaultEllipsoid; import org.apache.sis.referencing.operation.transform.InterpolatedGeocentricTransform; import static java.lang.Float.parseFloat; +import static org.apache.sis.internal.util.Constants.DIM; /** @@ -324,7 +325,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation { case 2: break; case 3: withHeights = true; break; default: throw new InvalidParameterValueException(Errors.format( - Errors.Keys.IllegalArgumentValue_2, "dim", dim), "dim", dim); + Errors.Keys.IllegalArgumentValue_2, DIM, dim), DIM, dim); } final Path file = pg.getMandatoryValue(FILE); final DatumShiftGridFile<Angle,Length> grid = getOrLoad(file, diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java index 32f5e451d7..2718c11d54 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffineBetweenGeographic.java @@ -161,7 +161,7 @@ public abstract class GeocentricAffineBetweenGeographic extends GeocentricAffine SRC_SEMI_MINOR = builder.addName("src_semi_minor").createStrictlyPositive(Double.NaN, Units.METRE); TGT_SEMI_MAJOR = builder.addName("tgt_semi_major").createStrictlyPositive(Double.NaN, Units.METRE); TGT_SEMI_MINOR = builder.addName("tgt_semi_minor").createStrictlyPositive(Double.NaN, Units.METRE); - DIMENSION = builder.addName("dim").setRequired(false).createBounded(Integer.class, 2, 3, null); + DIMENSION = builder.addName(Constants.DIM).setRequired(false).createBounded(Integer.class, 2, 3, null); } /** diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java index 0ab4bd17ea..f4a7dfa526 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeographicToGeocentric.java @@ -91,7 +91,7 @@ public final class GeographicToGeocentric extends GeodeticOperation { public static final ParameterDescriptorGroup PARAMETERS; static { final ParameterBuilder builder = builder(); - DIMENSION = builder.addName(Citations.SIS, "dim").setRequired(false).createBounded(Integer.class, 2, 3, 3); + DIMENSION = builder.addName(Citations.SIS, Constants.DIM).setRequired(false).createBounded(Integer.class, 2, 3, 3); PARAMETERS = builder .addIdentifier("9602") .addName("Geographic/geocentric conversions") diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java index 3e6c8d232c..9c728a6578 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java @@ -209,7 +209,7 @@ public final class Molodensky extends GeocentricAffineBetweenGeographic { final int n = dim; // Unboxing. if (n != 2 && n != 3) { throw new InvalidParameterValueException(Errors.format( - Errors.Keys.IllegalArgumentValue_2, "dim", dim), "dim", dim); + Errors.Keys.IllegalArgumentValue_2, Constants.DIM, dim), Constants.DIM, dim); } sourceDimensions = targetDimensions = n; } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java index 061a620e73..a3564bedb2 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/AbstractParameterDescriptor.java @@ -319,7 +319,7 @@ public abstract class AbstractParameterDescriptor extends AbstractIdentifiedObje /** * Formats this descriptor as a pseudo-<cite>Well Known Text</cite> element. The WKT specification - * does not define any representation of parameter descriptors. Apache SIS fallback on a list of + * does not define any representation of parameter descriptors. Apache SIS fallbacks on a list of * {@linkplain DefaultParameterDescriptor#formatTo(Formatter) descriptors}. * The text formatted by this method is {@linkplain Formatter#setInvalidWKT flagged as invalid WKT}. * diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/FilteredParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/FilteredParameters.java new file mode 100644 index 0000000000..569ff9561f --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/FilteredParameters.java @@ -0,0 +1,114 @@ +/* + * 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.parameter; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import org.opengis.parameter.GeneralParameterDescriptor; +import org.opengis.parameter.GeneralParameterValue; +import org.apache.sis.internal.util.UnmodifiableArrayList; +import org.apache.sis.util.ComparisonMode; + + +/** + * Wraps the given group of parameters, but hiding some parameters. + * This is used for hiding contextual parameters such as "semi_major". + * Hidden parameters will still be provided if explicitly requested. + * This filtered list is unmodifiable. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +final class FilteredParameters extends UnmodifiableParameterValueGroup { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 4880548875706032438L; + + /** + * The filtered parameter values. + */ + @SuppressWarnings("serial") // Not statically typed as Serializable. + private final GeneralParameterValue[] filtered; + + /** + * Creates a filtered view of given parameters. + */ + private FilteredParameters(final UnmodifiableParameterValueGroup source, final GeneralParameterValue[] filtered) { + super(source); + this.filtered = filtered; + } + + /** + * Creates a filtered view of given parameters. + * This method takes a snapshot of descriptor list using the given filter. + * No reference to that filter is kept after this method execution. + * + * @param source the group of parameters where values are actually stored. + * @param filter filter for deciding whether to keep a parameter. + * @return the filtered parameters. May be {@code source} itself. + */ + static UnmodifiableParameterValueGroup create(final UnmodifiableParameterValueGroup source, + final Predicate<? super GeneralParameterDescriptor> filter) + { + if (source != null && filter != null) { + final List<GeneralParameterValue> sources = source.values(); + final GeneralParameterValue[] filtered = new GeneralParameterValue[sources.size()]; + int count = 0; + for (final GeneralParameterValue value : sources) { + if (filter.test(value.getDescriptor())) { + filtered[count++] = value; + } + } + if (count != filtered.length) { + return new FilteredParameters(source, Arrays.copyOf(filtered, count)); + } + } + return source; // Nothing to filter. + } + + /** + * Returns a filtered view over the parameter value. + */ + @Override + public List<GeneralParameterValue> values() { + return UnmodifiableArrayList.wrap(filtered); + } + + /** + * Compares the specified object with this parameter for equality. + */ + @Override + public boolean equals(final Object object, final ComparisonMode mode) { + if (super.equals(object, mode) && mode == ComparisonMode.STRICT) { + final FilteredParameters other = (FilteredParameters) object; + return Arrays.equals(filtered, other.filtered); + } + return false; + } + + /** + * Returns a hash value for this parameter. + */ + @Override + public int hashCode() { + return super.hashCode() ^ filtered.length; + } +} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java index fc7ef71141..791b8ac013 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/Parameters.java @@ -19,6 +19,7 @@ package org.apache.sis.parameter; import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.function.Predicate; import java.io.Serializable; import javax.xml.bind.annotation.XmlTransient; import javax.measure.Unit; @@ -114,6 +115,18 @@ public abstract class Parameters implements ParameterValueGroup, Cloneable { protected Parameters() { } + /** + * Returns {@code true} if the given parameter group is a non-null instance created by {@code unmodifiable(…)}. + * + * @param parameters the parameter group to test. Can be {@code null}. + * @return whether the given parameters are non-null and unmodifiable. + * + * @since 1.3 + */ + public static boolean isUnmodifiable(final ParameterValueGroup parameters) { + return (parameters instanceof UnmodifiableParameterValueGroup); + } + /** * Returns the given parameter value group as an unmodifiable {@code Parameters} instance. * If the given parameters is already an unmodifiable instance of {@code Parameters}, @@ -132,6 +145,34 @@ public abstract class Parameters implements ParameterValueGroup, Cloneable { return UnmodifiableParameterValueGroup.create(parameters); } + /** + * Returns the given parameter value group as an unmodifiable {@code Parameters} instance + * with some parameters hidden. The hidden parameters are excluded from the list returned + * by {@link Parameters#values()}, but are otherwise still accessible when the hidden + * parameters is explicitly named in a call to {@link #parameter(String)}. + * + * <div class="note"><b>Use case:</b> + * this method is used for hiding parameters that should be inferred from the context. + * For example the {@code "semi_major"} and {@code "semi_minor"} parameters are included + * in the list of {@link org.opengis.referencing.operation.MathTransform} parameters + * because that class has no way to know the values if they are not explicitly provided. + * But those semi-axis length parameters should not be included in the list of + * {@link org.opengis.referencing.operation.CoordinateOperation} parameters + * because they are inferred from the context (the source and target CRS).</div> + * + * @param parameters the parameters to make unmodifiable, or {@code null}. + * @param filter specifies which source parameters to keep visible, or {@code null} if no filtering. + * @return an unmodifiable group with the parameters of the given group to keep visible, + * or {@code null} if the {@code parameters} argument was null. + * + * @since 1.3 + */ + public static Parameters unmodifiable(final ParameterValueGroup parameters, + final Predicate<? super GeneralParameterDescriptor> filter) + { + return FilteredParameters.create(UnmodifiableParameterValueGroup.create(parameters), filter); + } + /** * Returns the given parameter value group as a {@code Parameters} instance. * If the given parameters is already an instance of {@code Parameters}, then it is returned as-is. @@ -156,6 +197,8 @@ public abstract class Parameters implements ParameterValueGroup, Cloneable { @SuppressWarnings("CloneDoesntCallSuperClone") private static final class Wrapper extends Parameters implements Serializable { private static final long serialVersionUID = -5491790565456920471L; + + @SuppressWarnings("serial") // Not statically typed as Serializable. private final ParameterValueGroup delegate; Wrapper(final ParameterValueGroup delegate) {this.delegate = delegate;} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValueGroup.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValueGroup.java index f3e7b7c0f0..b5c94d2850 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValueGroup.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/UnmodifiableParameterValueGroup.java @@ -44,11 +44,11 @@ import org.apache.sis.internal.util.UnmodifiableArrayList; * the addition of {@code semi_major} and {@code semi_minor} parameters by {@code DefaultMathTransformFactory}. * * @author Martin Desruisseaux (Geomatys) - * @version 0.7 + * @version 1.3 * @since 0.7 * @module */ -final class UnmodifiableParameterValueGroup extends Parameters implements LenientComparable, Serializable { +class UnmodifiableParameterValueGroup extends Parameters implements LenientComparable, Serializable { /** * Serial number for inter-operability with different versions. */ @@ -70,6 +70,17 @@ final class UnmodifiableParameterValueGroup extends Parameters implements Lenien @SuppressWarnings("serial") // Not statically typed as Serializable. private final List<GeneralParameterValue> values; + /** + * Creates a copy of the given parameter group. + * This is used by {@link FilteredParameters} constructor only. + * + * @param group the group of values to copy. + */ + UnmodifiableParameterValueGroup(final UnmodifiableParameterValueGroup group) { + descriptor = group.descriptor; + values = group.values; + } + /** * Creates a new unmodifiable parameter group. * @@ -133,7 +144,7 @@ final class UnmodifiableParameterValueGroup extends Parameters implements Lenien * {@link #parameterIfExist(String)}. */ @Override - boolean isKnownImplementation() { + final boolean isKnownImplementation() { return true; } @@ -141,7 +152,7 @@ final class UnmodifiableParameterValueGroup extends Parameters implements Lenien * Returns the value in this group for the specified name. */ @Override - public ParameterValue<?> parameter(final String name) throws ParameterNotFoundException { + public final ParameterValue<?> parameter(final String name) throws ParameterNotFoundException { ArgumentChecks.ensureNonNull("name", name); final ParameterValue<?> value = parameterIfExist(name); if (value != null) { @@ -178,7 +189,7 @@ final class UnmodifiableParameterValueGroup extends Parameters implements Lenien * Operation not allowed. */ @Override - public ParameterValueGroup addGroup(final String name) throws IllegalStateException { + public final ParameterValueGroup addGroup(final String name) throws IllegalStateException { throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, ParameterValueGroup.class)); } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java index d9f95c1eda..ad0735e7f1 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/AbstractSingleOperation.java @@ -20,16 +20,19 @@ import java.util.Map; import java.util.List; import java.util.IdentityHashMap; import java.util.Objects; +import java.util.function.Predicate; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import org.opengis.util.GenericName; import org.opengis.util.FactoryException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.GeneralParameterValue; +import org.opengis.parameter.InvalidParameterValueException; import org.opengis.referencing.operation.SingleOperation; import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.MathTransform; @@ -64,7 +67,7 @@ import static org.apache.sis.util.Utilities.deepEquals; * {@link DefaultPassThroughOperation}.</p> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.2 + * @version 1.3 * @since 0.6 * @module */ @@ -102,6 +105,7 @@ class AbstractSingleOperation extends AbstractCoordinateOperation implements Sin * at XML unmarshalling time by {@link #setParameters(GeneralParameterValue[])}.</p> * * @see #getParameterValues() + * @see #setParameterValues(ParameterValueGroup, Map) */ @SuppressWarnings("serial") // Not statically typed as Serializable. ParameterValueGroup parameters; @@ -127,7 +131,7 @@ class AbstractSingleOperation extends AbstractCoordinateOperation implements Sin * However there is a few cases, for example the Molodenski transform, where we can not infer the * parameters easily because the operation is implemented by a concatenation of math transforms. */ - parameters = Parameters.unmodifiable(Containers.property(properties, CoordinateOperations.PARAMETERS_KEY, ParameterValueGroup.class)); + setParameterValues(Containers.property(properties, CoordinateOperations.PARAMETERS_KEY, ParameterValueGroup.class), null); } /** @@ -222,6 +226,60 @@ class AbstractSingleOperation extends AbstractCoordinateOperation implements Sin return (parameters != null) ? parameters : super.getParameterValues(); } + /** + * Sets the parameter values to a copy of given parameters, + * making sure that the parameters are compatible with the ones expected by the operation method. + * This method should be invoked by constructors only, after {@link #method} has been initialized. + * + * <p>If {@code ignore} is non-null, then parameters associated to {@link Boolean#TRUE} may be hidden. + * This situation happens when this operation has been initialized from a <em>defining conversion</em> + * and the caller refined the parameters using information provided by the math transform factory. + * On one hand, we want to take advantage of additional information present in {@code definition} + * such as OGC aliases (those information are often missing in {@link #method} if the latter + * is not a {@link org.apache.sis.referencing.operation.transform.MathTransformProvider}). + * But on the other hand, {@code definition} may contain contextual parameters (ellipsoid semi-axis lengths) + * which are unknown to {@link #method} and would cause an {@link InvalidParameterValueException} if we try + * to set them. We could replace {@link #method}, but if the latter was created from EPSG database it also + * contains metadata not present in {@code definition} descriptor. The compromise applied in this method is + * to keep {@link #method} as provided by user, also keep {@code definition} descriptor as supplied even if + * it is different than {@link #method} descriptor, but hide (not remove) parameters that are known + * to be redundant with information that can be inferred from the context.</p> + * + * @param definition the parameter to set, or {@code null} if none. + * @param ignore parameters to hide when the associated value is {@link Boolean#TRUE}, + * or {@code null} for no filtering. This map may be modified in-place. + */ + final void setParameterValues(ParameterValueGroup definition, final Map<String,Boolean> ignore) { + Predicate<GeneralParameterDescriptor> filter = null; + if (ignore != null) { + /* + * We enter in this block if creating a complete conversion from a defining conversion. + * The `method` instance typically come from an EPSG factory, so we want to keep it. + * The `definition` descriptor typically come from a `MathTransformProvider`, + * so we also want to keep it because it contains OGC aliases not present in `method`. + * For making both of them compatible, we will remove all contextual parameters + * (e.g. "semi_major") unless those parameters are known to `method`. + */ + for (final GeneralParameterDescriptor accepted : method.getParameters().descriptors()) { + ignore.remove(accepted.getName().getCode()); + for (final GenericName alias : accepted.getAlias()) { + ignore.remove(alias.tip().toString()); // Do not ignore known parameters. + } + } + filter = (candidate) -> { // Exclude only if `ignore.get(…) == true`. + Boolean isContextual = ignore.get(candidate.getName().getCode()); + if (isContextual == null) { + for (final GenericName alias : candidate.getAlias()) { + isContextual = ignore.get(alias.tip().toString()); + if (isContextual != null) break; + } + } + return !Boolean.TRUE.equals(isContextual); + }; + } + parameters = Parameters.unmodifiable(definition, filter); + } + /** * Compares this coordinate operation with the specified object for equality. If the {@code mode} argument * is {@link ComparisonMode#STRICT} or {@link ComparisonMode#BY_CONTRACT BY_CONTRACT}, then all available diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java index f61f85df2b..597b53e7a5 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java @@ -113,7 +113,7 @@ import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; * </ul> * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.3 * * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext) * @@ -641,7 +641,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { descriptor = GeocentricToGeographic.PARAMETERS; } parameters = descriptor.createValue(); - parameters.parameter("dim").setValue(geographic.getCoordinateSystem().getDimension()); + parameters.parameter(Constants.DIM).setValue(geographic.getCoordinateSystem().getDimension()); } else { /* * Coordinate system change (including change in the number of dimensions) without datum shift. @@ -681,9 +681,10 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { */ MathTransform transform = mtFactory.createParameterizedTransform(parameters, context); if (method == null) { - method = mtFactory.getLastMethodUsed(); + method = context.getMethodUsed(); } if (before != null) { + parameters = null; // Providing parameters would be misleading because they apply to only a step of the operation. transform = mtFactory.createConcatenatedTransform(before, transform); if (after != null) { transform = mtFactory.createConcatenatedTransform(transform, after); diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java index 52a76c2844..5e0d59b57b 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java @@ -1100,16 +1100,17 @@ class CoordinateOperationRegistry { if (mtFactory instanceof DefaultMathTransformFactory) { if (forward) sourceCRS = toGeodetic3D(sourceCRS, source3D); else targetCRS = toGeodetic3D(targetCRS, target3D); + final DefaultMathTransformFactory.Context context; final MathTransform mt; try { + context = ReferencingUtilities.createTransformContext(sourceCRS, targetCRS); mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform( - ((SingleOperation) op).getParameterValues(), - ReferencingUtilities.createTransformContext(sourceCRS, targetCRS)); + ((SingleOperation) op).getParameterValues(), context); } catch (InvalidGeodeticParameterException e) { log(null, e); break; } - operations.set(recreate(op, sourceCRS, targetCRS, mt, mtFactory.getLastMethodUsed())); + operations.set(recreate(op, sourceCRS, targetCRS, mt, context.getMethodUsed())); return true; } } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java index 8cec2a3414..3fff4153d9 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/DefaultConversion.java @@ -36,7 +36,6 @@ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactor import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.internal.referencing.ReferencingUtilities; import org.apache.sis.internal.referencing.Resources; -import org.apache.sis.parameter.Parameters; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Utilities; @@ -78,7 +77,7 @@ import org.apache.sis.util.Utilities; * by many objects and passed between threads without synchronization. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 0.7 + * @version 1.3 * * @see DefaultTransformation * @@ -212,9 +211,7 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver throw new IllegalArgumentException(Resources.forProperties(properties) .getString(Resources.Keys.UnspecifiedParameterValues)); } - if (parameters != null) { - this.parameters = Parameters.unmodifiable(parameters); - } + setParameterValues(parameters, null); checkDimensions(properties); } @@ -262,7 +259,8 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver context = ReferencingUtilities.createTransformContext(source, target); } transform = ((DefaultMathTransformFactory) factory).createParameterizedTransform(parameters, context); - parameters = Parameters.unmodifiable(context.getCompletedParameters()); + actual[0] = context.getMethodUsed(); + setParameterValues(context.getCompletedParameters(), context.getContextualParameters()); } else { /* * Fallback for non-SIS implementation. Equivalent to the above code, except that we can @@ -270,8 +268,8 @@ public class DefaultConversion extends AbstractSingleOperation implements Conver * the code should work anyway. */ transform = factory.createBaseToDerived(source, parameters, target.getCoordinateSystem()); + actual[0] = factory.getLastMethodUsed(); } - actual[0] = factory.getLastMethodUsed(); } else { /* * If the user specified explicitly a MathTransform, we may still need to swap or scale axes. diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java index c993ff98b5..f54d0dd35f 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java @@ -91,7 +91,7 @@ * for example by specifying the area of interest. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.2 + * @version 1.3 * @since 0.6 * @module */ diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java index 5d5925c287..f7543926c4 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java @@ -16,10 +16,11 @@ */ package org.apache.sis.referencing.operation.transform; -import java.util.IdentityHashMap; +import java.util.Set; import java.util.Map; +import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; @@ -27,8 +28,8 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.lang.reflect.Constructor; import java.io.Serializable; -import javax.measure.quantity.Length; import javax.measure.Unit; +import javax.measure.quantity.Length; import javax.measure.IncommensurableException; import org.opengis.parameter.ParameterValue; @@ -37,6 +38,7 @@ import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.parameter.InvalidParameterNameException; import org.opengis.parameter.InvalidParameterValueException; +import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeodeticCRS; import org.opengis.referencing.cs.CoordinateSystem; @@ -52,8 +54,9 @@ import org.opengis.util.FactoryException; import org.opengis.util.NoSuchIdentifierException; import org.apache.sis.io.wkt.Parser; -import org.apache.sis.internal.referencing.LazySet; +import org.apache.sis.internal.util.Strings; import org.apache.sis.internal.util.Constants; +import org.apache.sis.internal.referencing.LazySet; import org.apache.sis.internal.referencing.Formulas; import org.apache.sis.internal.referencing.CoordinateOperations; import org.apache.sis.internal.referencing.ReferencingUtilities; @@ -547,6 +550,12 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math * or anything else related to datum. Datum changes have dedicated {@link OperationMethod}, * for example <cite>"Longitude rotation"</cite> (EPSG:9601) for changing the prime meridian. * + * <h2>Scope</h2> + * Instances of this class should be short-lived + * (they exist only the time needed for creating a {@link MathTransform}) + * and should not be shared (because they provide no immutability guarantees). + * This class is not thread-safe. + * * @author Martin Desruisseaux (Geomatys) * @version 1.3 * @since 0.7 @@ -557,7 +566,7 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math /** * For cross-version compatibility. */ - private static final long serialVersionUID = 6963581151055917955L; + private static final long serialVersionUID = -239563539875674709L; /** * Coordinate system of the source or target points. @@ -574,8 +583,6 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math * The provider that created the parameterized {@link MathTransform} instance, or {@code null} * if this information does not apply. This field is used for transferring information between * {@code createParameterizedTransform(…)} and {@code swapAndScaleAxes(…)}. - * - * @todo We could make this information public as a replacement of {@link #getLastMethodUsed()}. */ private OperationMethod provider; @@ -586,10 +593,18 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math */ private ParameterValueGroup parameters; + /** + * Names of parameters which have been inferred from context. + * + * @see #getContextualParameters() + */ + private final Map<String,Boolean> contextualParameters; + /** * Creates a new context with all properties initialized to {@code null}. */ public Context() { + contextualParameters = new HashMap<>(); } /** @@ -799,11 +814,66 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math } /** - * Returns the parameter values used for the math transform creation, including the parameters completed - * by the factory. + * Returns the operation method used for the math transform creation. + * This is the same information than {@link #getLastMethodUsed()} but more stable + * (not affected by transforms created with other contexts). + * + * @return the operation method used by the factory. + * @throws IllegalStateException if {@link #createParameterizedTransform(ParameterValueGroup, Context)} + * has not yet been invoked. + * + * @see #getLastMethodUsed() + * + * @since 1.3 + */ + public OperationMethod getMethodUsed() { + if (provider != null) { + return provider; + } + throw new IllegalStateException(Resources.format(Resources.Keys.UnspecifiedParameterValues)); + } + + /** + * Returns the names of parameters that have been inferred from the context. + * The set of keys can contain any of {@code "dim"}, + * {@code "semi_major"}, {@code "semi_minor"}, + * {@code "src_semi_major"}, {@code "src_semi_minor"}, + * {@code "tgt_semi_major"}, {@code "tgt_semi_minor"} and/or + * {@code "inverse_flattening"}, depending on the operation method used. + * The parameters named in that set are included in the parameters + * returned by {@link #getCompletedParameters()}. + * + * <h4>Associated boolean values</h4> + * The associated boolean in the map tells whether the named parameter value is really contextual. + * The boolean is {@code FALSE} if the user explicitly specified a value in the parameters given to + * the {@link #createParameterizedTransform(ParameterValueGroup, Context)} method, + * and that value is different than the value inferred from the context. + * Such inconsistencies are also logged at {@link Level#WARNING}. + * In all other cases + * (no value specified by the user, or a value was specified but is consistent with the context), + * the associated boolean in the map is {@code TRUE}. + * + * <h4>Mutability</h4> + * The returned map is modifiable for making easier for callers to amend the contextual information. + * This map is not used by {@code Context} except for information purposes (e.g. in {@link #toString()}). + * In particular, modifications of this map have no incidence on the created {@link MathTransform}. + * + * @return names of parameters inferred from context. + * + * @since 1.3 + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") // Modifiable by method contract. + public Map<String,Boolean> getContextualParameters() { + return contextualParameters; + } + + /** + * Returns the parameter values used for the math transform creation, + * including the parameters completed by the factory. + * The parameters inferred from the context are listed by {@link #getContextualParameters()}. * * @return the parameter values used by the factory. - * @throws IllegalStateException if {@link DefaultMathTransformFactory#createParameterizedTransform(ParameterValueGroup, Context)} + * @throws IllegalStateException if {@link #createParameterizedTransform(ParameterValueGroup, Context)} * has not yet been invoked. */ public ParameterValueGroup getCompletedParameters() { @@ -823,31 +893,40 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math * parameter value validity. This may result in an {@link InvalidParameterNameException} * or {@link InvalidParameterValueException} to be thrown.</p> * - * @param writable {@code true} if this method should also check that the parameters group is not an - * instance of {@code UnmodifiableParameterValueGroup}. Current implementation assumes - * that modifiable parameters are instances of {@link DefaultParameterValueGroup}. + * @param writable {@code true} if this method should also check that the parameters group is editable. * @throws IllegalArgumentException if the copy can not be performed because a parameter has * a unrecognized name or an illegal value. */ private void ensureCompatibleParameters(final boolean writable) throws IllegalArgumentException { final ParameterDescriptorGroup expected = provider.getParameters(); - if (parameters.getDescriptor() != expected || - (writable && (parameters instanceof Parameters) - && !(parameters instanceof DefaultParameterValueGroup))) - { + if (parameters.getDescriptor() != expected || (writable && Parameters.isUnmodifiable(parameters))) { final ParameterValueGroup copy = expected.createValue(); Parameters.copy(parameters, copy); parameters = copy; } } + /** + * Gets a parameter for which to infer a value from the context. + * The consistency flag is initially set to {@link Boolean#TRUE}. + * + * @param name name of the contextual parameter. + * @return the parameter. + * @throws ParameterNotFoundException if the parameter was not found. + */ + private ParameterValue<?> getContextualParameter(final String name) throws ParameterNotFoundException { + ParameterValue<?> parameter = parameters.parameter(name); + contextualParameters.put(name, Boolean.TRUE); // Add only if above line succeeded. + return parameter; + } + /** * Returns the value of the given parameter in the given unit, or {@code NaN} if the parameter is not set. * * <p><b>NOTE:</b> Do not merge this function with {@code ensureSet(…)}. We keep those two methods * separated in order to give to {@code createParameterizedTransform(…)} a "all or nothing" behavior.</p> */ - private static double getValue(final ParameterValue<?> parameter, final Unit<Length> unit) { + private static double getValue(final ParameterValue<?> parameter, final Unit<?> unit) { return (parameter.getValue() != null) ? parameter.doubleValue(unit) : Double.NaN; } @@ -908,8 +987,8 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math ParameterValue<?> mismatchedParam = null; double mismatchedValue = 0; try { - final ParameterValue<?> ap = parameters.parameter(semiMajor); - final ParameterValue<?> bp = parameters.parameter(semiMinor); + final ParameterValue<?> ap = getContextualParameter(semiMajor); + final ParameterValue<?> bp = getContextualParameter(semiMinor); final Unit<Length> unit = ellipsoid.getAxisUnit(); /* * The two calls to getValue(…) shall succeed before we write anything, in order to have a @@ -920,10 +999,12 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math final double b = getValue(bp, unit); final double tol = Units.METRE.getConverterTo(unit).convert(ELLIPSOID_PRECISION); if (ensureSet(ap, a, ellipsoid.getSemiMajorAxis(), unit, tol)) { + contextualParameters.put(semiMajor, Boolean.FALSE); mismatchedParam = ap; mismatchedValue = a; } if (ensureSet(bp, b, ellipsoid.getSemiMinorAxis(), unit, tol)) { + contextualParameters.put(semiMinor, Boolean.FALSE); mismatchedParam = bp; mismatchedValue = b; } @@ -940,17 +1021,6 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math failure.addSuppressed(e); } } - final boolean isIvfDefinitive; - if (mismatchedParam != null) { - final LogRecord record = Resources.forLocale(null).getLogRecord(Level.WARNING, - Resources.Keys.MismatchedEllipsoidAxisLength_3, ellipsoid.getName().getCode(), - mismatchedParam.getDescriptor().getName().getCode(), mismatchedValue); - record.setLoggerName(Loggers.COORDINATE_OPERATION); - Logging.log(DefaultMathTransformFactory.class, "createParameterizedTransform", record); - isIvfDefinitive = false; - } else { - isIvfDefinitive = inverseFlattening && ellipsoid.isIvfDefinitive(); - } /* * Following is specific to Apache SIS. We use this non-standard API for allowing the * NormalizedProjection class (our base class for all map projection implementations) @@ -958,8 +1028,14 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math * instead of the semi-major axis length. It makes a small difference in the accuracy * of the eccentricity parameter. */ - if (isIvfDefinitive) try { - parameters.parameter(Constants.INVERSE_FLATTENING).setValue(ellipsoid.getInverseFlattening()); + if (mismatchedParam == null && inverseFlattening && ellipsoid.isIvfDefinitive()) try { + final ParameterValue<?> ep = getContextualParameter(Constants.INVERSE_FLATTENING); + final double e = getValue(ep, Units.UNITY); + if (ensureSet(ep, e, ellipsoid.getInverseFlattening(), Units.UNITY, 1E-10)) { + contextualParameters.put(Constants.INVERSE_FLATTENING, Boolean.FALSE); + mismatchedParam = ep; + mismatchedValue = e; + } } catch (ParameterNotFoundException e) { /* * Should never happen with Apache SIS implementation, but may happen if the given parameters come @@ -969,6 +1045,18 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math Logging.recoverableException(getLogger(Loggers.COORDINATE_OPERATION), DefaultMathTransformFactory.class, "createParameterizedTransform", e); } + /* + * If a parameter was explicitly specified by user but has a value inconsistent with the context, + * log a warning. In addition, the associated boolean value in `contextualParameters` map should + * have been set to `Boolean.FALSE`. + */ + if (mismatchedParam != null) { + final LogRecord record = Resources.forLocale(null).getLogRecord(Level.WARNING, + Resources.Keys.MismatchedEllipsoidAxisLength_3, ellipsoid.getName().getCode(), + mismatchedParam.getDescriptor().getName().getCode(), mismatchedValue); + record.setLoggerName(Loggers.COORDINATE_OPERATION); + Logging.log(DefaultMathTransformFactory.class, "createParameterizedTransform", record); + } } return failure; } @@ -1054,7 +1142,7 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math RuntimeException failure = null; if (sourceCS != null) try { ensureCompatibleParameters(true); - final ParameterValue<?> p = parameters.parameter("dim"); // Really `parameters`, not `userParams`. + final ParameterValue<?> p = getContextualParameter(Constants.DIM); if (p.getValue() == null) { p.setValue(sourceCS.getDimension()); } @@ -1065,6 +1153,48 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math failure = setEllipsoid(getTargetEllipsoid(), "tgt_semi_major", "tgt_semi_minor", false, failure); return failure; } + + /** + * Returns a string representation of this context for debugging purposes. + * Current implementation write the name of source/target coordinate systems and ellipsoids. + * If {@linkplain #getContextualParameters() contextual parameters} have already been inferred, + * then their names are appended with inconsistent parameters (if any) written on a separated line. + * + * @return a string representation of this context. + */ + @Override + public String toString() { + final Object[] properties = { + "sourceCS", sourceCS, "sourceEllipsoid", sourceEllipsoid, + "targetCS", targetCS, "targetEllipsoid", targetEllipsoid + }; + for (int i=1; i<properties.length; i += 2) { + final IdentifiedObject value = (IdentifiedObject) properties[i]; + if (value != null) properties[i] = value.getName(); + } + String text = Strings.toString(getClass(), properties); + if (!contextualParameters.isEmpty()) { + final StringBuilder b = new StringBuilder(text); + boolean isContextual = true; + do { + boolean first = true; + for (final Map.Entry<String,Boolean> entry : contextualParameters.entrySet()) { + if (entry.getValue() == isContextual) { + if (first) { + first = false; + b.append(System.lineSeparator()) + .append(isContextual ? "Contextual parameters" : "Inconsistencies").append(": "); + } else { + b.append(", "); + } + b.append(entry.getKey()); + } + } + } while ((isContextual = !isContextual) == false); + text = b.toString(); + } + return text; + } } /** @@ -1190,13 +1320,6 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math throw e; } finally { lastMethod.set(method); // May be null in case of failure, which is intended. - if (context != null) { - context.provider = null; - /* - * For now we conservatively reset the provider information to null. But if we choose to - * make that information public in a future SIS version, then we would remove this code. - */ - } } return transform; } @@ -1463,7 +1586,9 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math context.targetEllipsoid = ellipsoid; } final ParameterValueGroup pg = getDefaultParameters(operation); - if (cs.getDimension() < 3) pg.parameter("dim").setValue(2); // Apache SIS specific parameter. + if (cs.getDimension() < 3) { + pg.parameter(Constants.DIM).setValue(2); // Apache SIS specific parameter. + } return createParameterizedTransform(pg, context); } } @@ -1647,6 +1772,7 @@ public class DefaultMathTransformFactory extends AbstractFactory implements Math * @return the last method used by a {@code create(…)} constructor, or {@code null} if unknown of unsupported. * * @see #createParameterizedTransform(ParameterValueGroup, Context) + * @see Context#getMethodUsed() */ @Override public OperationMethod getLastMethodUsed() { diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java index 27233c28dd..30d9b7d88c 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java @@ -48,7 +48,7 @@ import static org.apache.sis.test.ReferencingAssert.*; * All tests performed by this class are two-dimensional. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.3 * * @param <G> the type of geometric objects, either {@link GeneralEnvelope} or {@link java.awt.geom.Rectangle2D}. * @@ -271,8 +271,9 @@ public abstract strictfp class TransformTestCase<G> extends TestCase { * @see org.apache.sis.referencing.operation.CoordinateOperationRegistry#inverse(SingleOperation) */ private static Conversion inverse(final Conversion conversion) throws NoninvertibleTransformException { - return new DefaultConversion(IdentifiedObjects.getProperties(conversion), conversion.getTargetCRS(), - conversion.getSourceCRS(), null, conversion.getMethod(), conversion.getMathTransform().inverse()); + return new DefaultConversion(IdentifiedObjects.getProperties(conversion, Conversion.IDENTIFIERS_KEY), + conversion.getTargetCRS(), conversion.getSourceCRS(), null, + conversion.getMethod(), conversion.getMathTransform().inverse()); } /** diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java index 1d9de64c96..0ae272c746 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java @@ -31,7 +31,7 @@ import org.apache.sis.util.Static; * creates itself the instance to be tested. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.3 * @since 0.5 * @module */ @@ -126,6 +126,13 @@ public final class Constants extends Static { */ public static final byte CRS1 = 1; + /** + * Name of a SIS-specific parameter for setting the number of dimensions of a coordinate operation. + * This constant should be used only in the context of coordinate operations, not in other contexts + * (e.g. not for the netCDF attribute of the same name). + */ + public static final String DIM = "dim"; + /** * The netCDF parameter name for the Earth radius. */