sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 04/04: Avoid direct reference to Java Topology Suite (JTS) library from the ST_Transform class. Instead, use the Geometries intenal class which will delegate to JTS, ESRI or Java2D depending which libraries are on the classpath. This commit also relaxes restrictions on type of arguments (first expression may not be an instance of FeatureExpression, and second expression does not need to be a literal).
Date Thu, 15 Aug 2019 15:41:25 GMT
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 43187e63bb22d3198db762ac7fdccf018695db2c
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Aug 15 17:26:28 2019 +0200

    Avoid direct reference to Java Topology Suite (JTS) library from the ST_Transform class.
    Instead, use the Geometries intenal class which will delegate to JTS, ESRI or Java2D
    depending which libraries are on the classpath. This commit also relaxes restrictions
    on type of arguments (first expression may not be an instance of FeatureExpression,
    and second expression does not need to be a literal).
---
 .../java/org/apache/sis/filter/NamedFunction.java  |  22 +++
 .../src/main/java/org/apache/sis/filter/SQLMM.java |  13 +-
 .../java/org/apache/sis/filter/ST_Transform.java   | 189 +++++++++++++++------
 .../sis/internal/feature/FeatureExpression.java    |   4 +-
 .../apache/sis/internal/feature/Geometries.java    | 129 +++++++++++++-
 .../java/org/apache/sis/internal/feature/JTS.java  |  47 ++++-
 .../org/apache/sis/internal/feature/jts/JTS.java   |  57 +++++--
 .../java/org/apache/sis/util/ArgumentChecks.java   |  25 +++
 8 files changed, 415 insertions(+), 71 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/NamedFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/NamedFunction.java
index d5a0b46..8bc8bb1 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/NamedFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/NamedFunction.java
@@ -18,6 +18,8 @@ package org.apache.sis.filter;
 
 import java.util.List;
 import java.util.Collection;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
 import org.opengis.filter.expression.Expression;
 import org.opengis.filter.expression.ExpressionVisitor;
 import org.opengis.filter.expression.Function;
@@ -25,7 +27,9 @@ import org.opengis.filter.expression.Literal;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.internal.feature.FeatureExpression;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.internal.util.CollectionsExt;
 
 
 /**
@@ -147,6 +151,24 @@ abstract class NamedFunction extends Node implements Function {
     }
 
     /**
+     * Returns the type of results computed by the parameters at given index, or {@code null}
if unknown.
+     * If the expression implements {@link FeatureExpression}, its {@code expectedType(valueType)}
method
+     * will be invoked. Otherwise this method returns the single property of the given feature
type if it
+     * contains exactly one property, or returns {@code null} otherwise.
+     *
+     * @param  parameter  index of the expression for which to get the result type.
+     * @param  valueType  the type of features on which to apply the expression at given
index.
+     * @return expected expression result type, or {@code null} if unknown.
+     */
+    final PropertyType expectedType(final int parameter, final FeatureType valueType) {
+        final Expression exp = parameters.get(parameter);
+        if (exp instanceof FeatureExpression) {
+            return ((FeatureExpression) exp).expectedType(valueType);
+        }
+        return CollectionsExt.singletonOrNull(valueType.getProperties(true));
+    }
+
+    /**
      * Implementation of the visitor pattern.
      */
     @Override
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/SQLMM.java b/core/sis-feature/src/main/java/org/apache/sis/filter/SQLMM.java
index 8d4dc3c..8a5f9ab 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/SQLMM.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/SQLMM.java
@@ -16,8 +16,9 @@
  */
 package org.apache.sis.filter;
 
-import java.util.Collections;
 import java.util.Set;
+import java.util.Collections;
+import org.opengis.util.FactoryException;
 import org.opengis.filter.expression.Expression;
 import org.opengis.filter.expression.Function;
 import org.apache.sis.util.ArgumentChecks;
@@ -65,9 +66,13 @@ public final class SQLMM implements FunctionRegister {
         for (int i=0; i<parameters.length; i++) {
             ArgumentChecks.ensureNonNullElement("parameters", i, parameters[i]);
         }
-        switch (name) {
-            case ST_Transform.NAME: return new ST_Transform(parameters);
-            default: throw new IllegalArgumentException("Unknown function " + name);
+        try {
+            switch (name) {
+                case ST_Transform.NAME: return new ST_Transform(parameters);
+                default: throw new IllegalArgumentException("Unknown function " + name);
+            }
+        } catch (FactoryException e) {
+            throw new IllegalArgumentException(e);
         }
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Transform.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Transform.java
index 8b84b41..33059da 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Transform.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Transform.java
@@ -16,11 +16,10 @@
  */
 package org.apache.sis.filter;
 
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.internal.feature.FeatureExpression;
-import org.apache.sis.internal.feature.jts.JTS;
-import org.apache.sis.referencing.CRS;
-import org.locationtech.jts.geom.Geometry;
+import java.util.Objects;
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
@@ -29,88 +28,180 @@ import org.opengis.filter.expression.Literal;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.FeatureExpression;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
  * An expression which transforms a geometry from one CRS to another CRS.
+ * This expression expects two arguments:
+ *
+ * <ol class="verbose">
+ *   <li>An expression returning a geometry object. The evaluated value shall be an
instance of
+ *       one of the implementations enumerated in {@link org.apache.sis.setup.GeometryLibrary}.</li>
+ *   <li>An expression returning the target CRS. This is typically a {@link Literal},
i.e. a constant for all geometries,
+ *       but this implementation allows the expression to return different CRS for different
geometries
+ *       for example depending on the number of dimensions. This CRS can be specified in
different ways:
+ *     <ul>
+ *       <li>As a {@link CoordinateReferenceSystem} instance.</li>
+ *       <li>As a {@link String} instance of the form {@code "EPSG:xxxx"}, a URL or
a URN.</li>
+ *       <li>As an {@linl Integer} instance specifying an EPSG code.</li>
+ *     </ul>
+ *   </li>
+ * </ol>
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
 final class ST_Transform extends NamedFunction implements FeatureExpression {
     /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -5769818355081378907L;
+
+    /**
      * Name of this function as defined by SQL/MM standard.
      */
     static final String NAME = "ST_Transform";
 
-    private final CoordinateReferenceSystem outCrs;
+    /**
+     * Identifier of the coordinate reference system in which to transform the geometry.
+     * This identifier is specified by the second expression and is stored in order to
+     * avoid computing {@link #targetCRS} many times when the SRID does not change.
+     */
+    private transient Object srid;
+
+    /**
+     * The coordinate reference system in which to transform the geometry, or {@code null}
+     * if not yet determined. This field is recomputed when the {@link #srid} change.
+     */
+    private transient CoordinateReferenceSystem targetCRS;
+
+    /**
+     * Whether the {@link #targetCRS} is defined by a literal.
+     * If {@code true}, then {@link #targetCRS} shall be effectively final.
+     */
+    private final boolean literalCRS;
 
     /**
      * Creates a new function with the given parameters. It is caller's responsibility to
ensure
      * that the given array is non-null, has been cloned and does not contain null elements.
+     *
+     * @throws IllegalArgumentException if the number of arguments is not equal to 2.
+     * @throws FactoryException if CRS can not be constructed from the second expression.
      */
-    ST_Transform(final Expression[] parameters) {
+    ST_Transform(final Expression[] parameters) throws FactoryException {
         super(parameters);
-        if (parameters.length != 2) {
-            throw new IllegalArgumentException("Reproject function expect 2 parameters, an
expression for the geometry and a literal for the target CRS.");
-        }
-        if (!(parameters[0] instanceof FeatureExpression)) {
-            throw new IllegalArgumentException("First expression must be a FeatureExpression");
-        }
-        if (!(parameters[1] instanceof Literal)) {
-            throw new IllegalArgumentException("Second expression must be a Literal");
-        }
-        final Object crsObj = parameters[1].evaluate(null);
-        if (crsObj instanceof CoordinateReferenceSystem) {
-            outCrs = (CoordinateReferenceSystem) crsObj;
-        } else if (crsObj instanceof Number) {
-            try {
-                this.outCrs = CRS.forCode("EPSG:" + crsObj);
-            } catch (FactoryException ex) {
-                throw new IllegalArgumentException("Requested CRS" + crsObj + "is undefined.\n"+ex.getMessage(),
ex);
-            }
-        } else if (crsObj instanceof String) {
-            try {
-                this.outCrs = CRS.forCode((String) crsObj);
-            } catch (FactoryException ex) {
-                throw new IllegalArgumentException("Requested CRS" + crsObj + "is undefined.\n"+ex.getMessage(),
ex);
-            }
-        } else {
-            throw new IllegalArgumentException("Second expression must be a Literal with
a CRS, Number or String value");
+        ArgumentChecks.ensureExpectedCount("parameters", 2, parameters.length);
+        final Expression crs = parameters[1];
+        literalCRS = (crs instanceof Literal);
+        if (literalCRS) {
+            setTargetCRS(((Literal) crs).getValue());
         }
     }
 
+    /**
+     * Returns the name of this function, which is {@value #NAME}.
+     */
     @Override
     public String getName() {
         return NAME;
     }
 
+    /**
+     * Invoked on deserialization for restoring the {@link #targetCRS} field.
+     *
+     * @param  in  the input stream from which to deserialize an attribute.
+     * @throws IOException if an I/O error occurred while reading or if the stream contains
invalid data.
+     * @throws ClassNotFoundException if the class serialized on the stream is not on the
classpath.
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException
{
+        in.defaultReadObject();
+        if (literalCRS) try {
+            setTargetCRS(((Literal) parameters.get(1)).getValue());
+        } catch (FactoryException e) {
+            throw (IOException) new InvalidObjectException(e.getLocalizedMessage()).initCause(e);
+        }
+    }
+
+    /**
+     * Sets {@link #targetCRS} to a coordinate reference system inferred from the given value.
+     * The CRS argument shall be the result of {@code parameters.get(1).evaluate(object)}.
+     *
+     * @throws FactoryException if no CRS can be created from the given object.
+     */
+    private void setTargetCRS(final Object crs) throws FactoryException {
+        if (crs instanceof CoordinateReferenceSystem) {
+            targetCRS = (CoordinateReferenceSystem) crs;
+        } else {
+            final String code;
+            if (crs instanceof String) {
+                code = (String) crs;
+            } else if (crs instanceof Integer) {
+                code = Constants.EPSG + ':' + crs;
+            } else {
+                throw new InvalidGeodeticParameterException(crs == null
+                        ? Errors.format(Errors.Keys.UnspecifiedCRS)
+                        : Errors.format(Errors.Keys.IllegalCRSType_1, crs.getClass()));
+            }
+            targetCRS = CRS.forCode(code);
+        }
+        srid = crs;
+    }
+
+    /**
+     * Evaluates the first expression as a geometry object, transforms that geometry to the
CRS given
+     * by the second expression and returns the result.
+     */
     @Override
-    public Object evaluate(final Object object) {
-        final Expression inGeometry = parameters.get(0);
-        Geometry geometry = inGeometry.evaluate(object, Geometry.class);
+    public Object evaluate(final Object value) {
+        Object geometry = parameters.get(0).evaluate(value);
         if (geometry != null) try {
-            return JTS.transform(geometry, outCrs);
-        } catch (TransformException | FactoryException e) {
+            final CoordinateReferenceSystem targetCRS;
+            if (literalCRS) {
+                targetCRS = this.targetCRS;             // No need to synchronize because
effectively final.
+            } else {
+                final Object crs = parameters.get(1).evaluate(value);
+                synchronized (this) {
+                    if (!Objects.equals(crs, srid)) {
+                        setTargetCRS(crs);
+                    }
+                    targetCRS = this.targetCRS;         // Must be inside synchronized block.
+                }
+            }
+            return Geometries.transform(geometry, targetCRS);
+        } catch (FactoryException | TransformException e) {
             warning(e);
         }
         return null;
     }
 
+    /**
+     * Returns the expected type of values produced by this expression when a feature of
the given
+     * type is evaluated.
+     *
+     * @param  valueType  the type of features on which to apply the expression.
+     * @return expected expression result type.
+     * @throws IllegalArgumentException if this method can not determine the property type
for the given feature type.
+     */
     @Override
-    public PropertyType expectedType(final FeatureType type) {
-        final FeatureExpression inGeometry = (FeatureExpression) parameters.get(0);
-
-        PropertyType expectedType = inGeometry.expectedType(type);
-        if (!(expectedType instanceof AttributeType)) {
-            throw new IllegalArgumentException("First expression must result in a Geometric
attribute");
-        }
-        AttributeType att = (AttributeType) expectedType;
-        if (!Geometry.class.isAssignableFrom(att.getValueClass())) {
-            throw new IllegalArgumentException("First expression must result in a Geometric
attribute");
+    public PropertyType expectedType(final FeatureType valueType) {
+        final PropertyType expectedType = expectedType(0, valueType);
+        if (expectedType instanceof AttributeType<?>) {
+            AttributeType<?> att = (AttributeType<?>) expectedType;
+            if (Geometries.isKnownType(att.getValueClass())) {
+                return new FeatureTypeBuilder().addAttribute(att).setCRS(literalCRS ? targetCRS
: null).build();
+            }
         }
-        return new FeatureTypeBuilder().addAttribute(att).setCRS(outCrs).build();
+        throw new IllegalArgumentException("First expression must result in a geometric attribute");
       // TODO: localize.
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index 1be7829..4667488 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
@@ -38,9 +38,9 @@ public interface FeatureExpression {
      * {@link org.opengis.feature.AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
      * but not an {@link org.opengis.feature.Operation}.
      *
-     * @param  type  the type of features on which to apply this expression.
+     * @param  valueType  the type of features on which to apply this expression.
      * @return expected expression result type.
      * @throws IllegalArgumentException if this method can not determine the property type
for the given feature type.
      */
-    PropertyType expectedType(FeatureType type);
+    PropertyType expectedType(FeatureType valueType);
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
index a30b2a2..40dd2e1 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java
@@ -19,8 +19,13 @@ package org.apache.sis.internal.feature;
 import java.util.Iterator;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import org.apache.sis.util.logging.Logging;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.math.Vector;
@@ -145,6 +150,35 @@ public abstract class Geometries<G> {
     }
 
     /**
+     * If the given geometry is an implementation of this library, returns its coordinate
reference system.
+     * Otherwise returns {@code null}. The default implementation returns {@code null} because
only a few
+     * geometry implementations can store CRS information.
+     *
+     * @see #tryTransform(Object, CoordinateOperation, CoordinateReferenceSystem)
+     */
+    CoordinateReferenceSystem tryGetCoordinateReferenceSystem(Object point) throws FactoryException
{
+        return null;
+    }
+
+    /**
+     * Gets the Coordinate Reference System (CRS) from the given geometry. If no CRS information
is found or
+     * if the geometry implementation can not store this information, then this method returns
{@code null}.
+     *
+     * @param  geometry the geometry from which to get the CRS, or {@code null}.
+     * @return the coordinate reference system, or {@code null}.
+     * @throws FactoryException if the CRS is defined by a SRID code and that code can not
be used.
+     *
+     * @see #transform(Object, CoordinateReferenceSystem)
+     */
+    public static CoordinateReferenceSystem getCoordinateReferenceSystem(final Object geometry)
throws FactoryException {
+        for (Geometries<?> g = implementation; g != null; g = g.fallback) {
+            CoordinateReferenceSystem crs = g.tryGetCoordinateReferenceSystem(geometry);
+            if (crs != null) return crs;
+        }
+        return null;
+    }
+
+    /**
      * If the given point is an implementation of this library, returns its coordinate.
      * Otherwise returns {@code null}.
      */
@@ -160,6 +194,7 @@ public abstract class Geometries<G> {
      * @return the coordinate of the given point as an array of length 2 or 3,
      *         or {@code null} if the given object is not a recognized implementation.
      *
+     * @see #getCoordinateReferenceSystem(Object)
      * @see #createPoint(double, double)
      */
     public static double[] getCoordinate(final Object point) {
@@ -317,6 +352,98 @@ public abstract class Geometries<G> {
     }
 
     /**
+     * Tries to transforms the given geometry to the specified Coordinate Reference System
(CRS),
+     * or returns {@code null} if this method can not perform this operation on the given
object.
+     * Exactly one of {@code operation} and {@code targetCRS} shall be non-null. If operation
is
+     * null and geometry has no Coordinate Reference System, a {@link TransformException}
is thrown.
+     *
+     * <p>Current default implementation returns {@code null} because current Apache
SIS implementation
+     * supports geometry transformations only with JTS geometries.</p>
+     *
+     * @param  geometry   the geometry to transform.
+     * @param  operation  the coordinate operation to apply, or {@code null}.
+     * @param  targetCRS  the target coordinate reference system, or {@code null}.
+     * @return the transformed geometry, or the same geometry if it is already in target
CRS.
+     *
+     * @see #tryGetCoordinateReferenceSystem(Object)
+     */
+    Object tryTransform(Object geometry, CoordinateOperation operation, CoordinateReferenceSystem
targetCRS)
+            throws FactoryException, TransformException
+    {
+        return null;
+    }
+
+    /**
+     * Transforms the given geometry using the given coordinate operation.
+     * If the geometry or the operation is null, then the geometry is returned unchanged.
+     * If the geometry uses a different CRS than the source CRS of the given operation,
+     * then a new operation to the target CRS will be used.
+     *
+     * <p>This method is preferred to {@link #transform(Object, CoordinateReferenceSystem)}
+     * when possible because not all geometry libraries store the CRS of their objects.</p>
+     *
+     * @param  geometry   the geometry to transform, or {@code null}.
+     * @param  operation  the coordinate operation to apply, or {@code null}.
+     * @return the transformed geometry, or the same geometry if it is already in target
CRS.
+     * @throws FactoryException if transformation to the target CRS can not be constructed.
+     * @throws TransformException if the given geometry can not be transformed.
+     */
+    public static Object transform(final Object geometry, final CoordinateOperation operation)
+            throws FactoryException, TransformException
+    {
+        /*
+         * Do NOT check MathTransform.isIdentity() below because the source CRS may not match
+         * the geometry CRS. This verification will be done by tryTransform(…) implementation.
+         */
+        if (geometry != null && operation != null) {
+            for (Geometries<?> g = implementation; g != null; g = g.fallback) {
+                final Object result = g.tryTransform(geometry, operation, null);
+                if (result != null) {
+                    return result;
+                }
+            }
+            if (!operation.getMathTransform().isIdentity()) {
+                final int dimension = ReferencingUtilities.getDimension(operation.getTargetCRS());
+                throw unsupported(dimension != 0 ? dimension : 2);
+            }
+        }
+        return geometry;
+    }
+
+    /**
+     * Transforms the given geometry to the specified Coordinate Reference System (CRS).
+     * If the given CRS or the given geometry is null, the geometry is returned unchanged.
+     * If the geometry has no Coordinate Reference System, a {@link TransformException} is
thrown.
+     *
+     * <p>Consider using {@link #transform(Object, CoordinateOperation)} instead of
this method
+     * as much as possible, both for performance reasons and because not all geometry libraries
+     * provide information about the CRS of their geometries.</p>
+     *
+     * @param  geometry   the geometry to transform, or {@code null}.
+     * @param  targetCRS  the target coordinate reference system, or {@code null}.
+     * @return the transformed geometry, or the same geometry if it is already in target
CRS.
+     * @throws FactoryException if transformation to the target CRS can not be constructed.
+     * @throws TransformException if the given geometry has no CRS or can not be transformed.
+     *
+     * @see #getCoordinateReferenceSystem(Object)
+     */
+    public static Object transform(final Object geometry, final CoordinateReferenceSystem
targetCRS)
+            throws FactoryException, TransformException
+    {
+        if (geometry != null && targetCRS != null) {
+            for (Geometries<?> g = implementation; g != null; g = g.fallback) {
+                final Object result = g.tryTransform(geometry, null, targetCRS);
+                if (result != null) {
+                    return result;
+                }
+            }
+            final int dimension = ReferencingUtilities.getDimension(targetCRS);
+            throw unsupported(dimension != 0 ? dimension : 2);
+        }
+        return geometry;
+    }
+
+    /**
      * Returns an error message for an unsupported geometry object.
      *
      * @param  dimension  number of dimensions (2 or 3) requested for the geometry object.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
index 20e2f6d..8a9c76a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
@@ -20,6 +20,10 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Iterator;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
@@ -125,7 +129,7 @@ final class JTS extends Geometries<Geometry> {
         } else {
             return null;
         }
-        final double z = pt.z;
+        final double z = pt.getZ();
         final double[] coord;
         if (Double.isNaN(z)) {
             coord = new double[2];
@@ -139,6 +143,21 @@ final class JTS extends Geometries<Geometry> {
     }
 
     /**
+     * If the given geometry is an implementation of this library, returns its coordinate
reference system.
+     * Otherwise returns {@code null}.
+     *
+     * @see #tryTransform(Object, CoordinateOperation, CoordinateReferenceSystem)
+     */
+    @Override
+    final CoordinateReferenceSystem tryGetCoordinateReferenceSystem(final Object geometry)
throws FactoryException {
+        if (geometry instanceof Geometry) {
+            return org.apache.sis.internal.feature.jts.JTS.getCoordinateReferenceSystem((Geometry)
geometry);
+        } else {
+            return super.tryGetCoordinateReferenceSystem(geometry);
+        }
+    }
+
+    /**
      * Creates a two-dimensional point from the given coordinate.
      *
      * @return the point for the given coordinate values.
@@ -264,4 +283,30 @@ add:    for (;;) {
         toLineString(coordinates, lines);
         return toGeometry(lines);
     }
+
+    /**
+     * Tries to transforms the given geometry to the specified Coordinate Reference System
(CRS),
+     * or returns {@code null} if this method can not perform this operation on the given
object.
+     * Exactly one of {@code operation} and {@code targetCRS} shall be non-null.
+     *
+     * @param  geometry   the geometry to transform.
+     * @param  operation  the coordinate operation to apply, or {@code null}.
+     * @param  targetCRS  the target coordinate reference system, or {@code null}.
+     * @return the transformed geometry, or the same geometry if it is already in target
CRS.
+     *
+     * @see #tryGetCoordinateReferenceSystem(Object)
+     */
+    @Override
+    Geometry tryTransform(final Object geometry, final CoordinateOperation operation, final
CoordinateReferenceSystem targetCRS)
+            throws FactoryException, TransformException
+    {
+        if (geometry instanceof Geometry) {
+            if (operation != null) {
+                return org.apache.sis.internal.feature.jts.JTS.transform((Geometry) geometry,
operation);
+            } else {
+                return org.apache.sis.internal.feature.jts.JTS.transform((Geometry) geometry,
targetCRS);
+            }
+        }
+        return null;
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java
index 41ac172..b766a06 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java
@@ -102,6 +102,32 @@ public final class JTS extends Static {
     }
 
     /**
+     * Finds an operation between the given CRS valid in the given area of interest.
+     * This method does not verify the CRS of the given geometry.
+     *
+     * @param  sourceCRS       the CRS of source coordinates.
+     * @param  targetCRS       the CRS of target coordinates.
+     * @param  areaOfInterest  the area of interest.
+     * @return the mathematical operation from {@code sourceCRS} to {@code targetCRS}.
+     * @throws FactoryException if the operation can not be created.
+     */
+    private static CoordinateOperation findOperation(final CoordinateReferenceSystem sourceCRS,
+                                                     final CoordinateReferenceSystem targetCRS,
+                                                     final Geometry areaOfInterest)
+            throws FactoryException
+    {
+        DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox();
+        try {
+            final Envelope e = areaOfInterest.getEnvelopeInternal();
+            bbox.setBounds(new Envelope2D(sourceCRS, e.getMinX(), e.getMinY(), e.getWidth(),
e.getHeight()));
+        } catch (TransformException ex) {
+            bbox = null;
+            Logging.ignorableException(Logging.getLogger(Loggers.GEOMETRY), JTS.class, "transform",
ex);
+        }
+        return CRS.findOperation(sourceCRS, targetCRS, bbox);
+    }
+
+    /**
      * Transforms the given geometry to the specified Coordinate Reference System (CRS).
      * If the given CRS or the given geometry is null, the geometry is returned unchanged.
      * If the geometry has no Coordinate Reference System, a {@link TransformException} is
thrown.
@@ -114,11 +140,11 @@ public final class JTS extends Static {
      * @param  geometry   the geometry to transform, or {@code null}.
      * @param  targetCRS  the target coordinate reference system, or {@code null}.
      * @return the transformed geometry, or the same geometry if it is already in target
CRS.
-     * @throws TransformException if the given geometry has no CRS or can not be transformed.
      * @throws FactoryException if transformation to the target CRS can not be constructed.
+     * @throws TransformException if the given geometry has no CRS or can not be transformed.
      */
     public static Geometry transform(Geometry geometry, final CoordinateReferenceSystem targetCRS)
-            throws TransformException, FactoryException
+            throws FactoryException, TransformException
     {
         if (geometry != null && targetCRS != null) {
             final CoordinateReferenceSystem sourceCRS = getCoordinateReferenceSystem(geometry);
@@ -126,33 +152,36 @@ public final class JTS extends Static {
                 throw new TransformException(Errors.format(Errors.Keys.UnspecifiedCRS));
             }
             if (!Utilities.equalsIgnoreMetadata(sourceCRS, targetCRS)) {
-                DefaultGeographicBoundingBox areaOfInterest = new DefaultGeographicBoundingBox();
-                try {
-                    final Envelope e = geometry.getEnvelopeInternal();
-                    areaOfInterest.setBounds(new Envelope2D(sourceCRS, e.getMinX(), e.getMinY(),
e.getWidth(), e.getHeight()));
-                } catch (TransformException ex) {
-                    areaOfInterest = null;
-                    Logging.ignorableException(Logging.getLogger(Loggers.GEOMETRY), JTS.class,
"transform", ex);
-                }
-                geometry = transform(geometry, CRS.findOperation(sourceCRS, targetCRS, areaOfInterest));
+                geometry = transform(geometry, findOperation(sourceCRS, targetCRS, geometry));
             }
         }
         return geometry;
     }
 
     /**
-     * Transform the given geometry using the given coordinate operation.
+     * Transforms the given geometry using the given coordinate operation.
      * If the geometry or the operation is null, then the geometry is returned unchanged.
+     * If the source CRS is not equals to the geometry CRS, a new operation is inferred.
      *
      * @todo Handle antimeridian case.
      *
      * @param  geometry   the geometry to transform, or {@code null}.
      * @param  operation  the coordinate operation to apply, or {@code null}.
      * @return the transformed geometry, or the same geometry if it is already in target
CRS.
+     * @throws FactoryException if transformation to the target CRS can not be constructed.
      * @throws TransformException if the given geometry can not be transformed.
      */
-    public static Geometry transform(Geometry geometry, final CoordinateOperation operation)
throws TransformException {
+    public static Geometry transform(Geometry geometry, CoordinateOperation operation)
+            throws FactoryException, TransformException
+    {
         if (geometry != null && operation != null) {
+            final CoordinateReferenceSystem sourceCRS = operation.getSourceCRS();
+            if (sourceCRS != null) {
+                final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(geometry);
+                if (crs != null && !Utilities.equalsIgnoreMetadata(sourceCRS, crs))
{
+                    operation = findOperation(crs, operation.getTargetCRS(), geometry);
+                }
+            }
             geometry = transform(geometry, operation.getMathTransform());
             geometry.setUserData(operation.getTargetCRS());
         }
@@ -169,7 +198,7 @@ public final class JTS extends Static {
      * @throws TransformException if the given geometry can not be transformed.
      */
     public static Geometry transform(Geometry geometry, MathTransform transform) throws TransformException
{
-        if (geometry != null && transform != null) {
+        if (geometry != null && transform != null && !transform.isIdentity())
{
             final GeometryCoordinateTransform gct = new GeometryCoordinateTransform(transform,
geometry.getFactory());
             geometry = gct.transform(geometry);
         }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
index 0c22f0f..b53c6bf 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java
@@ -247,6 +247,31 @@ public final class ArgumentChecks extends Static {
     }
 
     /**
+     * Ensures that a method receiving a variable number of arguments got the expected count.
+     * If {@code actual} = {@code expected}, then this method does nothing.
+     * Otherwise a message saying "Too few" or "Too many arguments" is thrown.
+     *
+     * @param  name      the name of the argument to be checked. Used only if an exception
is thrown.
+     * @param  expected  expected number of arguments.
+     * @param  actual    actual number of arguments.
+     *
+     * @since 1.0
+     */
+    public static void ensureExpectedCount(final String name, final int expected, final int
actual) {
+        if (actual != expected) {
+            final String message;
+            if (actual == 0) {
+                message = Errors.format(Errors.Keys.EmptyArgument_1, name);
+            } else if (actual < expected) {
+                message = Errors.format(Errors.Keys.TooFewArguments_2, expected, actual);
+            } else {
+                message = Errors.format(Errors.Keys.TooManyArguments_2, expected, actual);
+            }
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
      * Ensures that the specified value is null or an instance assignable to the given type.
      * If this method does not thrown an exception, then the value can be casted to the class
      * represented by {@code expectedType} without throwing a {@link ClassCastException}.


Mime
View raw message