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 5fb125cf40ea5fa7a9fe5fe1cfa0d4c6b3c6808f
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jul 16 18:33:06 2025 +0200

    If `Feature` property contains a geometry without CRS, fallback on the CRS 
defined in the `FeatureType`.
---
 .../org/apache/sis/feature/EnvelopeOperation.java  |   2 +-
 .../apache/sis/filter/BinaryGeometryFilter.java    |  34 +--
 .../org/apache/sis/filter/BinarySpatialFilter.java |   5 +-
 .../main/org/apache/sis/filter/DistanceFilter.java |   5 +-
 .../apache/sis/filter/InvalidXPathException.java   |   2 +-
 .../sis/filter/internal/GeometryConverter.java     |  50 +++-
 .../sis/filter/internal/GeometryFromFeature.java   | 108 +++++++
 .../main/org/apache/sis/filter/internal/Node.java  |  28 +-
 .../apache/sis/filter/sqlmm/FunctionWithSRID.java  |   4 +-
 .../apache/sis/filter/sqlmm/GeometryParser.java    |   3 +-
 .../org/apache/sis/filter/sqlmm/ST_Transform.java  |   6 +-
 .../apache/sis/geometry/wrapper/Geometries.java    |  74 ++++-
 .../sis/geometry/wrapper/GeometryWithCRS.java      |  92 ------
 .../sis/geometry/wrapper/GeometryWrapper.java      | 316 ++++++++++++---------
 .../apache/sis/geometry/wrapper/esri/Wrapper.java  |   3 +-
 .../apache/sis/geometry/wrapper/j2d/Factory.java   |   2 +-
 .../sis/geometry/wrapper/j2d/PointWrapper.java     |  13 +-
 .../apache/sis/geometry/wrapper/j2d/Wrapper.java   |   8 +-
 .../apache/sis/geometry/wrapper/jts/Factory.java   |  28 +-
 .../org/apache/sis/geometry/wrapper/jts/JTS.java   |  45 +--
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |  79 +++---
 .../apache/sis/geometry/wrapper/jts/JTSTest.java   |   6 +-
 .../storage/sql/feature/GeometryGetterTest.java    |   8 +-
 .../sis/storage/geopackage/GpkgStoreTest.java      |   2 +-
 24 files changed, 520 insertions(+), 403 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
index fc464e7c87..349c759a98 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
@@ -436,7 +436,7 @@ final class EnvelopeOperation extends AbstractOperation {
                  * the actual CRS.
                  */
                 if (sourceCRS != null && targetCRS != null) try {
-                    if (op == null) {
+                    if (op == null || sourceCRS != op.getSourceCRS()) {
                         op = CRS.findOperation(sourceCRS, targetCRS, null);
                     }
                     if (!op.getMathTransform().isIdentity()) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index 685c3ea900..92502c388f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -18,8 +18,6 @@ package org.apache.sis.filter;
 
 import java.util.List;
 import javax.measure.Unit;
-import javax.measure.IncommensurableException;
-import org.opengis.util.FactoryException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.wrapper.Geometries;
@@ -27,6 +25,7 @@ import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.geometry.wrapper.SpatialOperationContext;
 import org.apache.sis.feature.privy.AttributeConvention;
 import org.apache.sis.filter.internal.Node;
+import org.apache.sis.util.Exceptions;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Filter;
@@ -81,6 +80,7 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
     /**
      * Creates a new binary function.
      *
+     * @param  library     the geometry library to use.
      * @param  geometry1   the first of the two expressions to be used by this 
function.
      * @param  geometry2   the second of the two expressions to be used by 
this function.
      * @param  systemUnit  if the CRS needs to be in some units of 
measurement, the {@link Unit#getSystemUnit()} value.
@@ -102,20 +102,20 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
         final int index;
         final Literal<R,?> literal;
         final GeometryWrapper value;
-        if (geometry2 instanceof Literal<?,?>) {
-            literal = (Literal<R,?>) geometry2;
-            value   = expression2.apply(null);
-            index   = 1;
-        } else if (geometry1 instanceof Literal<?,?>) {
-            literal = (Literal<R,?>) geometry1;
-            value   = expression1.apply(null);
-            index   = 0;
-        } else {
-            literal = null;
-            value   = null;
-            index   = -1;
-        }
         try {
+            if (geometry2 instanceof Literal<?,?>) {
+                literal = (Literal<R,?>) geometry2;
+                value   = expression2.apply(null);
+                index   = 1;
+            } else if (geometry1 instanceof Literal<?,?>) {
+                literal = (Literal<R,?>) geometry1;
+                value   = expression1.apply(null);
+                index   = 0;
+            } else {
+                literal = null;
+                value   = null;
+                index   = -1;
+            }
             context = new SpatialOperationContext(null, value, systemUnit, 
index);
             if (value != null) {
                 final GeometryWrapper gt = context.transform(value);
@@ -128,8 +128,8 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
                     }
                 }
             }
-        } catch (FactoryException | TransformException | 
IncommensurableException e) {
-            throw new InvalidFilterValueException(e);
+        } catch (Exception e) {
+            throw new InvalidFilterValueException(Exceptions.unwrap(e));
         }
         this.expression1 = expression1;
         this.expression2 = expression2;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
index 240cd00afc..3e03ba7213 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
@@ -23,6 +23,7 @@ import org.apache.sis.geometry.WraparoundMethod;
 import org.apache.sis.geometry.wrapper.SpatialOperationContext;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.geometry.wrapper.Geometries;
+import org.apache.sis.util.Exceptions;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Expression;
@@ -143,8 +144,8 @@ final class BinarySpatialFilter<R> extends 
BinaryGeometryFilter<R> implements Bi
             final GeometryWrapper right = expression2.apply(object);
             if (right != null) try {
                 return left.predicate(operatorType, right, context);
-            } catch (RuntimeException e) {
-                warning(e, true);
+            } catch (Exception e) {
+                warning(Exceptions.unwrap(e), true);
             }
         }
         return emptyResult();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
index 72d5a396fc..3fd8a10447 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
@@ -25,6 +25,7 @@ import org.opengis.geometry.Geometry;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.geometry.wrapper.SpatialOperationContext;
+import org.apache.sis.util.Exceptions;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.filter.Literal;
@@ -160,8 +161,8 @@ final class DistanceFilter<R> extends 
BinaryGeometryFilter<R> implements Distanc
             final GeometryWrapper right = expression2.apply(object);
             if (right != null) try {
                 return left.predicate(operatorType, right, distance, context);
-            } catch (RuntimeException e) {
-                warning(e, true);
+            } catch (Exception e) {
+                warning(Exceptions.unwrap(e), true);
             }
         }
         return emptyResult();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
index 3a49222670..39c1a5d428 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
@@ -21,7 +21,7 @@ import org.opengis.filter.InvalidFilterValueException;
 
 
 /**
- * Exceptions thrown when the XPath in an expression is invalid or unsupported.
+ * Exception thrown when the XPath in an expression is invalid or unsupported.
  * Apache SIS currently supports only a small subset of XPath syntax, mostly 
paths of
  * the form {@code "a/b/c"} (and not everywhere) and the {@code 
"Q{namespace}"} syntax.
  *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
index fbdff72b61..5e77717b8c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryConverter.java
@@ -29,17 +29,18 @@ import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.geometry.WraparoundMethod;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
+import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.filter.Optimization;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.coordinate.MismatchedDimensionException;
 import org.opengis.filter.Expression;
 import org.opengis.filter.InvalidFilterValueException;
+import org.opengis.coordinate.MismatchedDimensionException;
 
 
 /**
- * Expression whose results is a geometry wrapper. This converter evaluates 
another expression,
- * which is given at construction time, potentially converts the result then 
wraps it.
+ * Expression whose result is a geometry wrapper. This converter evaluates 
another expression,
+ * which is given at construction time, then wraps the result in a {@link 
GeometryWrapper}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
@@ -49,7 +50,7 @@ import org.opengis.filter.InvalidFilterValueException;
  *
  * @see org.apache.sis.filter.ConvertFunction
  */
-final class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpression<R, GeometryWrapper> {
+class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpression<R, GeometryWrapper> {
     /**
      * For cross-version compatibility.
      */
@@ -66,7 +67,10 @@ final class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpres
     final Geometries<G> library;
 
     /**
-     * The expression to be used by this operator.
+     * The expression which returns instances of a geometry library such as 
<abbr>JTS</abbr>.
+     * The objects returned by this expression shall be recognized by the 
{@linkplain #library},
+     * with the addition of the following special cases: {@link 
DirectPosition}, {@link Envelope},
+     * {@link GeographicBoundingBox} and (in some cases) {@code Feature}.
      *
      * @see #getParameters()
      */
@@ -74,14 +78,40 @@ final class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpres
     final Expression<R,?> expression;
 
     /**
-     * Creates a new converter expression.
+     * Creates a new converter for the given expression producing 
library-specific objects.
+     * This constructor is for subclasses. Use {@link #create(Geometries, 
Expression)} instead.
      *
      * @param  library     the geometry library to use.
-     * @param  expression  the expression providing source values.
+     * @param  expression  the expression providing geometric objects of the 
given library.
      */
-    public GeometryConverter(final Geometries<G> library, final 
Expression<R,?> expression) {
-        this.expression = Objects.requireNonNull(expression);
+    GeometryConverter(final Geometries<G> library, final Expression<R,?> 
expression) {
         this.library    = Objects.requireNonNull(library);
+        this.expression = Objects.requireNonNull(expression);
+    }
+
+    /**
+     * Creates a new converter for the given expression producing 
library-specific objects.
+     *
+     * @param  library     the geometry library to use.
+     * @param  expression  the expression providing geometric objects of the 
given library.
+     * @return the geometry converter.
+     */
+    @SuppressWarnings("unchecked")
+    public static <R,G> GeometryConverter<R,G> create(final Geometries<G> 
library, final Expression<R,?> expression) {
+        if (expression instanceof GeometryConverter<?,?>) {
+            final var candidate = (GeometryConverter<R,?>) expression;
+            if (library.equals(candidate.library)) {
+                return (GeometryConverter<R,G>) expression;
+            }
+            throw new InvalidFilterValueException(Resources.format(
+                    Resources.Keys.MixedGeometryImplementation_2,
+                    library.library, candidate.library.library));
+        }
+        GeometryConverter<?,G> candidate = 
GeometryFromFeature.tryCreate(library, expression);
+        if (candidate == null) {
+            return new GeometryConverter<>(library, expression);
+        }
+        return (GeometryConverter<R,G>) candidate;
     }
 
     /**
@@ -90,7 +120,7 @@ final class GeometryConverter<R,G> extends Node implements 
Optimization.OnExpres
      */
     @Override
     public Expression<R, GeometryWrapper> recreate(final Expression<R,?>[] 
effective) {
-        return new GeometryConverter<>(library, effective[0]);
+        return create(library, effective[0]);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
new file mode 100644
index 0000000000..178a668113
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/GeometryFromFeature.java
@@ -0,0 +1,108 @@
+/*
+ * 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.filter.internal;
+
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometry.wrapper.Geometries;
+import org.apache.sis.geometry.wrapper.GeometryWrapper;
+import org.apache.sis.feature.privy.AttributeConvention;
+import org.apache.sis.filter.privy.XPath;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.filter.ValueReference;
+import org.opengis.filter.InvalidFilterValueException;
+
+
+/**
+ * Expression whose input is a feature instance and output is a geometry 
wrapper.
+ * This converter evaluates another expression, which is given at construction 
time,
+ * then wraps the result in a {@link GeometryWrapper}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <G>  the geometry implementation type.
+ */
+final class GeometryFromFeature<G> extends GeometryConverter<Feature, G> {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -550060050444279386L;
+
+    /**
+     * Name of the property from which the geometry object is read, or {@code 
null} if none.
+     * This is used for fetching the default <abbr>CRS</abbr> when the 
geometry has none.
+     */
+    private final String propertyName;
+
+    /**
+     * Creates a new converter for the given expression producing 
library-specific objects.
+     *
+     * @param  library     the geometry library to use.
+     * @param  expression  the expression providing geometric objects of the 
given library.
+     */
+    private GeometryFromFeature(Geometries<G> library, Expression<Feature,?> 
expression, String propertyName) {
+        super(library, expression);
+        this.propertyName = propertyName;
+    }
+
+    /**
+     * Tries to create a new converter for the given expression.
+     *
+     * @param  library     the geometry library to use.
+     * @param  expression  the expression providing geometric objects of the 
given library.
+     * @return the geometry converter, or {@code null} if the given expression 
cannot be used.
+     */
+    static <G> GeometryFromFeature<G> tryCreate(final Geometries<G> library, 
final Expression<?,?> expression) {
+        if (Feature.class.isAssignableFrom(expression.getResourceClass()) && 
expression instanceof ValueReference<?,?>) {
+            final var xpath = new XPath(((ValueReference<?,?>) 
expression).getXPath());
+            if (xpath.path == null) {
+                /*
+                 * The expression type is actually <? extends R>, so it is not 
really correct to cast to <R>.
+                 * However, we are going to use <R> as input only, not as 
output. In such case, it is okay to
+                 * ignore the fact that <R> may be a subtype of `Feature`.
+                 */
+                @SuppressWarnings("unchecked")
+                final var ve = (Expression<Feature,?>) expression;
+                return new GeometryFromFeature<>(library, ve, xpath.tip);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Evaluates the expression and converts the value to a geometry wrapper.
+     * If the geometry library does not store the <abbr>CRS</abbr>,
+     * the coordinate reference system is taken from the feature.
+     *
+     * @param  input  the geometry to evaluate with this expression.
+     * @return the geometry wrapper, or {@code null} if the evaluated value is 
null.
+     * @throws InvalidFilterValueException if the expression result is not an 
instance of a supported type.
+     */
+    @Override
+    public GeometryWrapper apply(final Feature input) {
+        final GeometryWrapper wrapper = super.apply(input);
+        if (wrapper.getCoordinateReferenceSystem() == null) {
+            final CoordinateReferenceSystem crs = 
AttributeConvention.getCRSCharacteristic(input, propertyName);
+            if (crs != null) {
+                wrapper.setCoordinateReferenceSystem(crs);
+            }
+        }
+        return wrapper;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
index 18788702b8..5be96dd66d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
@@ -36,6 +36,7 @@ import org.apache.sis.filter.privy.WarningEvent;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.util.iso.Names;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
@@ -51,9 +52,8 @@ import org.opengis.feature.AttributeType;
 
 
 /**
- * Base class of Apache SIS implementation of OGC expressions, comparators or 
filters.
- * {@code Node} instances are associated together in a tree, which can be 
formatted
- * by {@link #toString()}.
+ * Base class of Apache <abbr>SIS</abbr> implementations of <abbr>OGC</abbr> 
expressions, comparators and filters.
+ * {@code Node} instances are organized in a tree which can be formatted by 
{@link #toString()}.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -94,15 +94,15 @@ public abstract class Node implements Serializable {
      * @see Expression#getFunctionName()
      */
     protected static <T> AttributeType<T> createType(final Class<T> type, 
final Object name) {
-        // We do not use `Map.of(…)` for letting the attribute type 
constructor do the null check.
+        // We do not use `Map.of(…)` for better exception message in case of 
null name.
         return new 
DefaultAttributeType<>(Collections.singletonMap(DefaultAttributeType.NAME_KEY, 
name),
                                           type, 1, 1, null, 
(AttributeType<?>[]) null);
     }
 
     /**
-     * Returns the most specialized class of the given pair of class. A 
specialized class is guaranteed to exist
+     * Returns the most specialized class of the given pair of classes. A 
specialized class is guaranteed to exist
      * if parametrized type safety has not been bypassed with unchecked casts, 
because {@code <R>} is always valid.
-     * However this method is not guaranteed to be able to find that 
specialized type, because it could be none of
+     * However, this method is not guaranteed to be able to find that 
specialized type, because it could be none of
      * the given arguments if {@code t1}, {@code t2} and {@code <R>} are 
interfaces with {@code <R>} extending both
      * {@code t1} and {@code t2}.
      *
@@ -169,33 +169,25 @@ public abstract class Node implements Serializable {
 
     /**
      * Returns an expression whose results is a geometry wrapper.
+     * Note that the {@code apply(R)} method of the returned expression may 
throw {@link BackingStoreException}.
      *
      * @param  <R>         the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
      * @param  <G>         the geometry implementation type.
      * @param  library     the geometry library to use.
-     * @param  expression  the expression providing source values.
+     * @param  expression  the expression providing geometry instances of the 
given library.
      * @return an expression whose results is a geometry wrapper.
      * @throws InvalidFilterValueException if the given expression is already 
a wrapper
      *         but for another geometry implementation.
      */
-    @SuppressWarnings("unchecked")
     protected static <R,G> Expression<R, GeometryWrapper> toGeometryWrapper(
             final Geometries<G> library, final Expression<R,?> expression)
     {
-        if (expression instanceof GeometryConverter<?,?>) {
-            final Geometries<?> other = ((GeometryConverter<?,?>) 
expression).library;
-            if (library.equals(other)) {
-                return (GeometryConverter<R,G>) expression;
-            }
-            throw new InvalidFilterValueException(Resources.format(
-                    Resources.Keys.MixedGeometryImplementation_2, 
library.library, other.library));
-        }
-        return new GeometryConverter<>(library, expression);
+        return GeometryConverter.create(library, expression);
     }
 
     /**
      * If the given exception was wrapped by {@link 
#toGeometryWrapper(Geometries, Expression)},
-     * returns the original expression. Otherwise returns the given expression.
+     * returns the original expression. Otherwise, returns the given 
expression as-is.
      *
      * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
      * @param  expression  the expression to unwrap.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
index 2d751dd31e..fc3b821449 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
@@ -87,10 +87,10 @@ abstract class FunctionWithSRID<R> extends 
SpatialFunction<R> {
      *
      * @param  operation   identification of the SQLMM operation.
      * @param  parameters  sub-expressions that will be evaluated to provide 
the parameters to the function.
-     * @param  hasSRID     whether the SRID is expected as one of {@link 
#PRESENT}, {@link #ABSENT} or {@link #MAYBE}.
+     * @param  hasSRID     whether the <abbr>SRID</abbr> is expected: {@link 
#PRESENT}, {@link #ABSENT} or {@link #MAYBE}.
      *
      * @todo The {@code MAYBE} flag could be removed if we know the type of 
value evaluated by the expression.
-     *       For now it exists mostly because the last parameter given to 
{@code ST_Point} can be of various types.
+     *       For now, it exists mostly because the last parameter given to 
{@code ST_Point} can be of various types.
      */
     FunctionWithSRID(final SQLMM operation, final Expression<R,?>[] 
parameters, int hasSRID) {
         super(operation, parameters);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
index af5ac750ac..c9fe68215e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/GeometryParser.java
@@ -20,6 +20,7 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -112,7 +113,7 @@ abstract class GeometryParser<R,G> extends 
GeometryConstructor<R,G> {
             }
             return library.getGeometry(result);
         } catch (Exception e) {
-            warning(e, false);
+            warning(Exceptions.unwrap(e), false);
         }
         return null;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
index 3d7b299ea4..0a3b7c8847 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/ST_Transform.java
@@ -49,7 +49,7 @@ import org.opengis.filter.InvalidFilterValueException;
  *   </li>
  * </ol>
  *
- * <h2>Limitation</h2>
+ * <h2>Limitations</h2>
  * <ul>
  *   <li>Current implementation ignores the <var>z</var> and <var>m</var> 
values.</li>
  *   <li>If the SRID is an integer, it is interpreted as an EPSG code.
@@ -122,8 +122,8 @@ final class ST_Transform<R> extends FunctionWithSRID<R> {
      * by the second expression and returns the result.
      *
      * @param  input  the object from which to get a geometry.
-     * @return the transformed geometry, or {@code null} if the given object 
is not an instance of
-     *         a supported geometry library (JTS, ERSI, Java2D…).
+     * @return the transformed geometry, or {@code null} if the given object 
is not an instance
+     *         of a supported geometry library (<abbr>JTS</abbr>, 
<abbr>ERSI</abbr>, Java2D…).
      */
     @Override
     public Object apply(final R input) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
index fd33834c06..00c44ba879 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java
@@ -31,6 +31,7 @@ import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.WraparoundMethod;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.feature.privy.AttributeConvention;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.privy.AxisDirections;
@@ -38,15 +39,19 @@ import org.apache.sis.system.Loggers;
 import org.apache.sis.math.Vector;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.BackingStoreException;
+
+// Specific to the geoapi-3.1 and 4.0 branches:
+import org.opengis.feature.Feature;
 
 // Specific to the geoapi-4.0 branch:
 import org.opengis.coordinate.MismatchedDimensionException;
 
 
 /**
- * Utility methods on geometric objects defined in libraries outside Apache 
SIS.
+ * Utility methods on geometric objects defined in libraries outside Apache 
<abbr>SIS</abbr>.
  * We use this class for isolating dependencies from the {@code 
org.apache.feature} package
- * to ESRI's API or to Java Topology Suite (JTS) API.
+ * to <abbr>ESRI</abbr>'s <abbr>API</abbr> or to Java Topology Suite 
(<abbr>JTS</abbr>) API.
  * This gives us a single place to review if we want to support different 
geometry libraries,
  * or if Apache SIS come with its own implementation.
  *
@@ -96,8 +101,8 @@ public abstract class Geometries<G> implements Serializable {
     /**
      * The fallback implementation to use if the default one is not available.
      * This is set by {@link GeometryFactories} and should not change after 
initialization.
-     * We do not synchronize accesses to this field because we keep it stable 
after
-     * {@link GeometryFactories} class initialization.
+     * We do not synchronize the accesses to this field because we keep the 
field value stable
+     * after {@link GeometryFactories} class initialization.
      *
      * <h4>Temporarily permitted change</h4>
      * {@link GeometryFactories#setStandard(Geometries)} temporarily permits a 
change of this field,
@@ -108,7 +113,7 @@ public abstract class Geometries<G> implements Serializable 
{
 
     /**
      * {@code true} if {@link #pointClass} is not a subtype of {@link 
#rootClass}.
-     * This is true for Java2D and false for JTS and ESRI libraries.
+     * This is true for Java2D and false for <abbr>JTS</abbr> and 
<abbr>ESRI</abbr> libraries.
      */
     private final transient boolean isPointClassDistinct;
 
@@ -212,6 +217,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  wrapper  the wrapper for which to get the geometry, or {@code 
null}.
      * @return the geometry instance of the library requested by user, or 
{@code null} if the given wrapper was null.
      * @throws ClassCastException if the given wrapper is not an instance of 
the class expected by this factory.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see #getGeometryClass(GeometryType)
      * @see #implementation(Object)
@@ -227,13 +233,56 @@ public abstract class Geometries<G> implements 
Serializable {
         return wrapper.implementation();
     }
 
+    /**
+     * Wraps the geometry stored in a property of the given feature. This 
method should be used
+     * instead of {@link #wrap(Object)} when the value come from a feature 
instance in order to
+     * allow <abbr>SIS</abbr> to fetch a default <abbr>CRS</abbr> when the 
geometry object does
+     * not specify the <abbr>CRS</abbr> itself.
+     *
+     * @param  feature   the feature from which wrap a geometry, or {@code 
null} if none.
+     * @param  property  the name of the property from which to get the 
default <abbr>CRS</abbr>.
+     * @return a wrapper for the geometry implementation of the given feature, 
or empty value.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
+     */
+    public static Optional<GeometryWrapper> wrap(final Feature feature, final 
String property) {
+        if (feature == null) {
+            return Optional.empty();
+        }
+        final Optional<GeometryWrapper> value = 
wrap(feature.getPropertyValue(property));
+        value.ifPresent((wrapper) -> {
+            if (wrapper.crs == null) {
+                wrapper.crs = 
AttributeConvention.getCRSCharacteristic(feature, property);
+            }
+        });
+        return value;
+    }
+
+    /**
+     * Wraps the default geometry of the given feature. This method should be 
used instead of {@link #wrap(Object)}
+     * when possible because it allows <abbr>SIS</abbr> to fetch a default 
<abbr>CRS</abbr> when the geometry object
+     * does not specify the <abbr>CRS</abbr> itself.
+     *
+     * @param  feature   the feature from which wrap the default geometry, or 
{@code null} if none.
+     * @return a wrapper for the geometry implementation of the given feature, 
or empty value.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
+     */
+    public static Optional<GeometryWrapper> wrap(final Feature feature) {
+        return wrap(feature, AttributeConvention.GEOMETRY);
+    }
+
     /**
      * Wraps the given geometry implementation if recognized.
      * If the given object is already an instance of {@link GeometryWrapper}, 
then it is returned as-is.
      * If the given object is not recognized, then this method returns an 
empty value.
      *
+     * <h4>Recommended alternative</h4>
+     * Prefers {@link #wrap(Feature)} for wrapping the default geometry of a 
feature instance.
+     * This is preferred for allowing <abbr>SIS</abbr> to fetch the default 
<abbr>CRS</abbr>
+     * from the feature type when that information was not present in the 
geometry object.
+     *
      * @param  geometry  the geometry instance to wrap (can be {@code null}).
      * @return a wrapper for the given geometry implementation, or empty value.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see #castOrWrap(Object)
      * @see #implementation(Object)
@@ -271,6 +320,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @return a wrapper for the given geometry implementation, or {@code 
null} if the given object was null.
      * @throws ClassCastException if the given object is not a wrapper or a 
geometry object
      *         of the implementation of the library identified by {@link 
#library}.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see #wrap(Object)
      */
@@ -325,6 +375,7 @@ public abstract class Geometries<G> implements Serializable 
{
      *
      * @param  point  the point to convert to a geometry.
      * @return the given point converted to a geometry.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public final GeometryWrapper createPoint(final DirectPosition point) {
         final Object geometry;
@@ -350,6 +401,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  x  the first coordinate value.
      * @param  y  the second coordinate value.
      * @return the point for the given coordinate values.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see Capability#SINGLE_PRECISION
      */
@@ -365,6 +417,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  x  the first coordinate value.
      * @param  y  the second coordinate value.
      * @return the point for the given coordinate values.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see GeometryWrapper#getPointCoordinates()
      */
@@ -379,6 +432,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  y  the second coordinate value.
      * @param  z  the third coordinate value.
      * @return the point for the given coordinate values.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see Capability#Z_COORDINATE
      * @see GeometryWrapper#getPointCoordinates()
@@ -396,6 +450,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  dimensions   the dimensions of the coordinate tuple.
      * @param  coordinates  a (x,y), (x,y,z), (x,y,m) or (x,y,z,m) coordinate 
tuple.
      * @return the point for the given coordinate values.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public abstract Object createPoint(boolean isFloat, Dimensions dimensions, 
DoubleBuffer coordinates);
 
@@ -412,6 +467,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  dimensions   the dimensions of the coordinate tuples.
      * @param  coordinates  sequence of (x,y), (x,y,z), (x,y,m) or (x,y,z,m) 
coordinate tuples.
      * @return the collection of points for the given points.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public abstract G createMultiPoint(boolean isFloat, Dimensions dimensions, 
DoubleBuffer coordinates);
 
@@ -438,6 +494,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @throws UnsupportedOperationException if the geometry library cannot 
create the requested collection.
      * @throws IllegalArgumentException if a polygon was requested but the 
given coordinates do not make
      *         a closed shape (linear ring).
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public abstract G createPolyline(boolean polygon, boolean isFloat, 
Dimensions dimensions, DoubleBuffer... coordinates);
 
@@ -453,6 +510,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @throws UnsupportedOperationException if the geometry library cannot 
create the requested collection.
      * @throws IllegalArgumentException if a polygon was requested but the 
given coordinates do not make
      *         a closed shape (linear ring).
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public final G createPolyline(final boolean polygon, final Dimensions 
dimensions, final Vector... coordinates) {
         boolean isFloat = true;
@@ -485,6 +543,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  geometries  the polygons or linear rings to put in a 
multi-polygons.
      * @return the multi-polygon.
      * @throws ClassCastException if an element in the array is not an 
implementation of backing library.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @todo Consider a more general method creating a multi-polygon or 
multi-line depending on object types,
      *       or returning a more primitive geometry type if the given array 
contains only one element.
@@ -512,6 +571,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @throws IllegalArgumentException if the given geometry type is not 
supported.
      * @throws ClassCastException if {@code components} is not an array or a 
collection of supported geometry components.
      * @throws ArrayStoreException if {@code components} is an array with 
invalid component type.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public abstract GeometryWrapper createFromComponents(GeometryType type, 
Object components);
 
@@ -523,6 +583,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @throws IllegalArgumentException if the library has no specific type 
for the given components.
      * @throws ClassCastException if {@code components} is not an array or a 
collection of supported geometry components.
      * @throws ArrayStoreException if {@code components} is an array with 
invalid component type.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     protected final GeometryWrapper createFromComponents(final Object 
components) {
         final Class<?> c = components.getClass();
@@ -544,6 +605,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  expand  whether to expand the envelope to full axis range if 
there is a wraparound.
      * @param  addPts  whether to allow insertion of intermediate points on 
edges of axis domains.
      * @return a polyline made of a sequence of at least 5 points describing 
the given rectangle.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     private GeometryWrapper createGeometry2D(final Envelope envelope, final 
int xd, final int yd,
                                              final boolean expand, final 
boolean addPts)
@@ -622,6 +684,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  envelope  the envelope to convert.
      * @param  strategy  how to resolve wrap-around ambiguities on the 
envelope.
      * @return the envelope as a polygon, or potentially as two polygons in 
{@link WraparoundMethod#SPLIT} case.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public GeometryWrapper toGeometry2D(final Envelope envelope, final 
WraparoundMethod strategy) {
         int xd = 0, yd = 1;
@@ -694,6 +757,7 @@ public abstract class Geometries<G> implements Serializable 
{
      * @param  geometry  the geometry to wrap.
      * @return wrapper for the given geometry.
      * @throws ClassCastException if the given geometry is not an instance of 
valid type.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      *
      * @see #castOrWrap(Object)
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWithCRS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWithCRS.java
deleted file mode 100644
index 7b594beff1..0000000000
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWithCRS.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.geometry.wrapper;
-
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.util.ArgumentChecks;
-
-
-/**
- * A geometry wrapper with a field for CRS information. This base class is 
used when the geometry implementation
- * to wrap does not store CRS information by itself. See {@link 
GeometryWrapper} for more information.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-public abstract class GeometryWithCRS extends GeometryWrapper {
-    /**
-     * The coordinate reference system, or {@code null} if unspecified.
-     */
-    private CoordinateReferenceSystem crs;
-
-    /**
-     * Creates a new instance initialized with null CRS.
-     */
-    protected GeometryWithCRS() {
-    }
-
-    /**
-     * Gets the Coordinate Reference System (CRS) of this geometry.
-     *
-     * @return the geometry CRS, or {@code null} if unknown.
-     */
-    @Override
-    public final CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        return crs;
-    }
-
-    /**
-     * Sets the coordinate reference system, which shall be two-dimensional.
-     *
-     * @param  crs  the coordinate reference system to set.
-     */
-    @Override
-    public final void setCoordinateReferenceSystem(final 
CoordinateReferenceSystem crs) {
-        ArgumentChecks.ensureDimensionMatches("crs", Geometries.BIDIMENSIONAL, 
crs);
-        this.crs = crs;
-    }
-
-    /**
-     * Returns {@code true} if the given geometry use the same CRS as this 
geometry, or conservatively
-     * returns {@code false} in case of doubt. This method should perform only 
a cheap test; it is used
-     * as a way to filter rapidly if {@link 
#transform(CoordinateReferenceSystem)} needs to be invoked.
-     *
-     * @param  other  the second geometry.
-     * @return {@code true} if the two geometries use equivalent CRS or if the 
CRS is undefined on both side,
-     *         or {@code false} in case of doubt.
-     */
-    @Override
-    public final boolean isSameCRS(final GeometryWrapper other) {
-        /*
-         * Identity comparison is often sufficient since all geometries 
typically share the same CRS.
-         * If they are not the same instance, a more expensive 
`equalsIgnoreMetadata(…)` method here
-         * would probably duplicate the work done later by the 
`transform(Geometry, …)` method.
-         */
-        return crs == ((GeometryWithCRS) other).crs;
-    }
-
-    /**
-     * Creates an initially empty envelope with the CRS of this geometry.
-     * If this geometry has no CRS, then a two-dimensional envelope is created.
-     * This is a convenience method for {@link #getEnvelope()} implementations.
-     *
-     * @return an initially empty envelope.
-     */
-    protected final GeneralEnvelope createEnvelope() {
-        return (crs != null) ? new GeneralEnvelope(crs) : new 
GeneralEnvelope(Geometries.BIDIMENSIONAL);
-    }
-}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
index 252da84b90..0ccc9fb2c4 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/GeometryWrapper.java
@@ -33,10 +33,13 @@ import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.filter.sqlmm.SQLMM;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.privy.ReferencingUtilities;
-import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Debug;
+import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.resources.Errors;
 
@@ -47,23 +50,44 @@ import org.opengis.geometry.TransfiniteSet;
 import org.opengis.geometry.complex.Complex;
 import org.opengis.filter.SpatialOperatorName;
 import org.opengis.filter.DistanceOperatorName;
-import org.opengis.filter.InvalidFilterValueException;
+
+// Specific to the geoapi-4.0 branch:
+import org.opengis.coordinate.MismatchedDimensionException;
 
 
 /**
- * Wraps a JTS, ESRI or Java2D geometry behind a {@code Geometry} interface.
+ * Wraps a <abbr>JTS</abbr>, <abbr>ESRI</abbr> or Java2D geometry behind a 
{@code Geometry} interface.
+ *
+ * <h4>Future plans</h4>
  * This is a temporary class to be refactored later as a more complete 
geometry framework.
  * The methods provided in this class are not committed API, and often not 
even clean API.
- * They are only utilities added for very specific Apache SIS needs and will 
certainly
- * change without warning in future Apache SIS version.
+ * They are only utilities added for very specific Apache <abbr>SIS</abbr> 
needs and will
+ * certainly change without warning in future Apache <abbr>SIS</abbr> versions.
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
  * @see Geometries#wrap(Object)
  */
 public abstract class GeometryWrapper extends AbstractGeometry implements 
Geometry {
+    /**
+     * The coordinate reference system, or {@code null} if unspecified.
+     * The value of this field should be set by subclass constructors.
+     *
+     * <h4>Where this value come from</h4>
+     * This information is sometime redundant with information stored in the 
geometry itself.
+     * However, even when the geometry library supports some kind of 
<abbr>SRID</abbr> field,
+     * the value in geometry instances is sometime 0 or null. In such case, 
this {@code crs}
+     * field may come from other sources such as characteristics of the {@code 
FeatureType}.
+     *
+     * @see #getCoordinateDimension()
+     * @see #getCoordinateReferenceSystem()
+     * @see #setCoordinateReferenceSystem(CoordinateReferenceSystem)
+     */
+    protected CoordinateReferenceSystem crs;
+
     /**
      * Creates a new geometry object.
+     * Subclasses should set the {@link #crs} field if known.
      */
     protected GeometryWrapper() {
     }
@@ -89,11 +113,11 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
     protected abstract Object implementation();
 
     /**
-     * Returns the Spatial Reference System Identifier (SRID) if available.
-     * The SRID is used in database such as PostGIS and is generally 
database-dependent.
-     * This is <em>not</em> necessarily an EPSG code, even if it is a common 
practice
-     * to use the same numerical values as EPSG. Note that the absence of SRID 
does
-     * not mean that {@link #getCoordinateReferenceSystem()} would return no 
CRS.
+     * Returns the Spatial Reference System Identifier (<abbr>SRID</abbr>) if 
available.
+     * The <abbr>SRID</abbr> is used in databases such as PostGIS and is 
generally database-dependent.
+     * This is <em>not</em> necessarily an <abbr>EPSG</abbr> code, even if it 
is a common practice to
+     * use the same numerical values as <abbr>EPSG</abbr>. Note that the 
absence of <abbr>SRID</abbr>
+     * does not mean that {@link #getCoordinateReferenceSystem()} would return 
no <abbr>CRS</abbr>.
      *
      * <p>Users should invoke the {@link #getCoordinateReferenceSystem()} 
method instead.
      * This {@code getSRID()} method is provided for classes such as {@code 
DataStore} backed by an SQL database.
@@ -108,26 +132,54 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
     }
 
     /**
-     * Gets the Coordinate Reference System (CRS) of this geometry. In some 
libraries (for example JTS) the CRS
-     * is stored in the {@link Geometries#rootClass} instances of that 
library. In other libraries (e.g. Java2D)
-     * the CRS is stored only in this {@code GeometryWrapper} instance.
+     * Gets the Coordinate Reference System (<abbr>CRS</abbr>) of the geometry.
+     * In some libraries such as <abbr>JTS</abbr>, some <abbr>CRS</abbr> 
information can be stored in the
+     * geometry object of that library. In other libraries such as Java2D, the 
<abbr>CRS</abbr> is stored
+     * only in this {@code GeometryWrapper} instance.
      *
-     * @return the geometry CRS, or {@code null} if unknown.
-     * @throws BackingStoreException if the CRS is defined by a SRID code and 
that code cannot be used.
+     * @return the geometry <abbr>CRS</abbr>, or {@code null} if unknown.
      */
     @Override
-    public abstract CoordinateReferenceSystem getCoordinateReferenceSystem();
+    public final CoordinateReferenceSystem getCoordinateReferenceSystem() {
+        return crs;
+    }
 
     /**
      * Sets the coordinate reference system.
      * This method should be invoked only for newly created geometries. If the 
geometry library supports
-     * user objects (e.g. JTS), there is no guarantee that this method will 
not overwrite user setting.
+     * user objects (e.g. JTS), there is no guarantee that this method will 
not overwrite user's setting.
      *
      * @param  crs  the coordinate reference system to set.
+     * @throws MismatchedDimensionException if the <abbr>CRS</abbr> does not 
have the expected number of dimensions.
      *
      * @see #transform(CoordinateReferenceSystem)
      */
-    public abstract void 
setCoordinateReferenceSystem(CoordinateReferenceSystem crs);
+    public void setCoordinateReferenceSystem(final CoordinateReferenceSystem 
crs) {
+        ArgumentChecks.ensureDimensionMatches("crs", getCoordinateDimension(), 
crs);
+        this.crs = crs;
+    }
+
+    /**
+     * Returns the dimension of the coordinates that define this geometry.
+     * It must be the same as the dimension of the coordinate reference system 
for this geometry.
+     *
+     * @return the coordinate dimension.
+     */
+    @Override
+    public int getCoordinateDimension() {
+        return Geometries.BIDIMENSIONAL;
+    }
+
+    /**
+     * Creates an initially empty envelope with the <abbr>CRS</abbr> of this 
geometry.
+     * If this geometry has no <abbr>CRS</abbr>, then a two- or 
three-dimensional envelope is created.
+     * This is a convenience method for {@link #getEnvelope()} implementations.
+     *
+     * @return an initially empty envelope.
+     */
+    protected final GeneralEnvelope createEnvelope() {
+        return (crs != null) ? new GeneralEnvelope(crs) : new 
GeneralEnvelope(getCoordinateDimension());
+    }
 
     /**
      * Returns the geometry bounding box, together with its coordinate 
reference system.
@@ -185,6 +237,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @param  paths  the points or polylines to merge in a single polyline 
instance.
      * @return the merged polyline (may be the underlying geometry of {@code 
this} but never {@code null}).
      * @throws ClassCastException if collection elements are not instances of 
the point or geometry class.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public abstract Object mergePolylines(final Iterator<?> paths);
 
@@ -198,23 +251,23 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @param  context   the preferred CRS and other context to use if 
geometry transformations are needed.
      * @return result of applying the specified predicate.
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
-     * @throws InvalidFilterValueException if an error occurred while 
executing the operation on given geometries.
+     * @throws FactoryException if transformation to the target 
<abbr>CRS</abbr> cannot be found.
+     * @throws TransformException if a geometry cannot be transformed.
+     * @throws IncommensurableException if a unit conversion was necessary but 
failed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
     public final boolean predicate(final DistanceOperatorName type, final 
GeometryWrapper other,
                                    final Quantity<Length> distance, final 
SpatialOperationContext context)
+            throws FactoryException, TransformException, 
IncommensurableException
     {
-        final GeometryWrapper[] geometries = new GeometryWrapper[] {this, 
other};
-        try {
-            if (context.transform(geometries)) {
-                double dv = distance.getValue().doubleValue();
-                final Unit<?> unit = 
ReferencingUtilities.getUnit(context.commonCRS);
-                if (unit != null) {
-                    dv = 
distance.getUnit().getConverterToAny(unit).convert(dv);
-                }
-                return geometries[0].predicateSameCRS(type, geometries[1], dv);
+        final var geometries = new GeometryWrapper[] {this, other};
+        if (context.transform(geometries)) {
+            double dv = distance.getValue().doubleValue();
+            final Unit<?> unit = 
ReferencingUtilities.getUnit(context.commonCRS);
+            if (unit != null) {
+                dv = distance.getUnit().getConverterToAny(unit).convert(dv);
             }
-        } catch (FactoryException | TransformException | 
IncommensurableException e) {
-            throw new InvalidFilterValueException(e);
+            return geometries[0].predicateSameCRS(type, geometries[1], dv);
         }
         /*
          * No common CRS. Consider that we have no intersection, no overlap, 
etc.
@@ -235,18 +288,18 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @param  context  the preferred CRS and other context to use if geometry 
transformations are needed.
      * @return result of applying the specified predicate.
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
-     * @throws InvalidFilterValueException if an error occurred while 
executing the operation on given geometries.
+     * @throws FactoryException if transformation to the target 
<abbr>CRS</abbr> cannot be found.
+     * @throws TransformException if a geometry cannot be transformed.
+     * @throws IncommensurableException if a unit conversion was necessary but 
failed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
     public final boolean predicate(final SpatialOperatorName type, final 
GeometryWrapper other,
                                    final SpatialOperationContext context)
+            throws FactoryException, TransformException, 
IncommensurableException
     {
-        final GeometryWrapper[] geometries = new GeometryWrapper[] {this, 
other};
-        try {
-            if (context.transform(geometries)) {
-                return geometries[0].predicateSameCRS(type, geometries[1]);
-            }
-        } catch (FactoryException | TransformException | 
IncommensurableException e) {
-            throw new InvalidFilterValueException(e);
+        final var geometries = new GeometryWrapper[] {this, other};
+        if (context.transform(geometries)) {
+            return geometries[0].predicateSameCRS(type, geometries[1]);
         }
         /*
          * No common CRS. Consider that we have no intersection, no overlap, 
etc.
@@ -256,7 +309,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
     }
 
     /**
-     * Applies a SQLMM operation on this geometry.
+     * Applies a <abbr>SQLMM</abbr> operation on this geometry.
      * This method shall be invoked only for operations without non-geometric 
parameters.
      *
      * @param  operation  the SQLMM operation to apply.
@@ -264,6 +317,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
      * @throws ClassCastException if the operation can only be executed on 
some specific geometry subclasses
      *         (for example polylines) and the wrapped geometry is not of that 
class.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public final Object operation(final SQLMM operation) {
         assert operation.geometryCount() == 1 && operation.maxParamCount == 1 
: operation;
@@ -273,7 +327,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
     }
 
     /**
-     * Applies a SQLMM operation on two geometries.
+     * Applies a <abbr>SQLMM</abbr> operation on two geometries.
      * This method shall be invoked only for operations without non-geometric 
parameters.
      * The second geometry is transformed to the same CRS as this geometry for 
conformance with SQLMM standard.
      *
@@ -284,18 +338,17 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @throws ClassCastException if the operation can only be executed on 
some specific geometry subclasses
      *         (for example polylines) and the wrapped geometry is not of that 
class.
      * @throws TransformException if it was necessary to transform the other 
geometry and that transformation failed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
-    public final Object operation(final SQLMM operation, final GeometryWrapper 
other)
-            throws TransformException
-    {
+    public final Object operation(final SQLMM operation, final GeometryWrapper 
other) throws TransformException {
         assert operation.geometryCount() == 2 && operation.maxParamCount == 2 
: operation;
-        final Object result = operationSameCRS(operation, toSameCRS(other), 
null);
+        final Object result = operationSameCRS(operation, 
other.transform(crs), null);
         assert isInstance(operation, result) : result;
         return result;
     }
 
     /**
-     * Applies a SQLMM operation on this geometry with one operation-specific 
argument.
+     * Applies a <abbr>SQLMM</abbr> operation on this geometry with one 
operation-specific argument.
      * The argument shall be non-null, unless the argument is optional.
      *
      * @param  operation  the SQLMM operation to apply.
@@ -304,12 +357,13 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
      * @throws ClassCastException if the operation can only be executed on 
some specific geometry subclasses
      *         (for example polylines) and the wrapped geometry is not of that 
class.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public final Object operationWithArgument(final SQLMM operation, final 
Object argument) {
         assert operation.geometryCount() == 1 && operation.maxParamCount == 2 
: operation;
-        if (argument == null && operation.minParamCount > 1) {
+        if (operation.minParamCount > 1) {
             // TODO: fetch argument name.
-            throw new 
NullPointerException(Errors.format(Errors.Keys.NullArgument_1, "arg1"));
+            ArgumentChecks.ensureNonNull("arg1", argument);
         }
         final Object result = operationSameCRS(operation, null, argument);
         assert isInstance(operation, result) : result;
@@ -317,7 +371,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
     }
 
     /**
-     * Applies a SQLMM operation on two geometries with one operation-specific 
argument.
+     * Applies a <abbr>SQLMM</abbr> operation on two geometries with one 
operation-specific argument.
      * The argument shall be non-null, unless the argument is optional.
      * The second geometry is transformed to the same CRS as this geometry for 
conformance with SQLMM standard.
      *
@@ -329,6 +383,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @throws ClassCastException if the operation can only be executed on 
some specific geometry subclasses
      *         (for example polylines) and the wrapped geometry is not of that 
class.
      * @throws TransformException if it was necessary to transform the other 
geometry and that transformation failed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
     public final Object operationWithArgument(final SQLMM operation, final 
GeometryWrapper other, final Object argument)
             throws TransformException
@@ -338,32 +393,11 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
             // TODO: fetch argument name.
             throw new 
NullPointerException(Errors.format(Errors.Keys.NullArgument_1, "arg2"));
         }
-        final Object result = operationSameCRS(operation, toSameCRS(other), 
argument);
+        final Object result = operationSameCRS(operation, 
other.transform(crs), argument);
         assert isInstance(operation, result) : result;
         return result;
     }
 
-    /**
-     * Transforms the {@code other} geometry to the same CRS as this geometry.
-     * This method should be cheap for the common case where the other geometry
-     * already uses the CRS, in which case it is returned unchanged.
-     *
-     * <p>If this geometry does not define a CRS, then current implementation
-     * returns the other geometry unchanged.</p>
-     *
-     * @param  other  the other geometry.
-     * @return the other geometry in the same CRS as this geometry.
-     * @throws TransformException if the other geometry cannot be transformed.
-     *         If may be because the other geometry does not define its CRS.
-     */
-    private GeometryWrapper toSameCRS(final GeometryWrapper other) throws 
TransformException {
-        if (isSameCRS(other)) {
-            return other;
-        }
-        final CoordinateReferenceSystem crs = getCoordinateReferenceSystem();
-        return (crs != null) ? other.transform(crs) : this;
-    }
-
     /**
      * Returns {@code true} if a result is of the expected type.
      * This is used for assertion purposes only.
@@ -374,7 +408,8 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
 
     /**
      * Applies a filter predicate between this geometry and another geometry.
-     * This method assumes that the two geometries are in the same CRS (this 
is not verified).
+     * This method assumes that the two geometries are in the same 
<abbr>CRS</abbr> (this is not verified).
+     * Conversions, if needed, shall be done by the caller.
      *
      * <p><b>Note:</b> {@link SpatialOperatorName#BBOX} is implemented by 
{@code NOT DISJOINT}.
      * It is caller's responsibility to ensure that one of the geometries is 
rectangular.</p>
@@ -383,6 +418,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @param  other  the other geometry to test with this geometry.
      * @return result of applying the specified predicate.
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     protected boolean predicateSameCRS(SpatialOperatorName type, 
GeometryWrapper other) {
         throw new 
UnsupportedOperationException(Geometries.unsupported(type.name()));
@@ -390,21 +426,25 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
 
     /**
      * Applies a filter predicate between this geometry and another geometry 
within a given distance.
-     * This method assumes that the two geometries are in the same CRS and 
that the unit of measurement
-     * is the same for {@code distance} than for axes (this is not verified).
+     * This method assumes that the two geometries are in the same 
<abbr>CRS</abbr> and that the unit
+     * of measurement is the same for {@code distance} than for axes (this is 
not verified).
+     * Conversions, if needed, shall be done by the caller.
      *
      * @param  type      the predicate operation to apply.
      * @param  other     the other geometry to test with this geometry.
      * @param  distance  distance to test between the geometries.
      * @return result of applying the specified predicate.
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     protected boolean predicateSameCRS(DistanceOperatorName type, 
GeometryWrapper other, double distance) {
         throw new 
UnsupportedOperationException(Geometries.unsupported(type.name()));
     }
 
     /**
-     * Applies a SQLMM operation on this geometry.
+     * Applies a <abbr>SQLMM</abbr> operation on this geometry.
+     * This method assumes that the two geometries are in the same 
<abbr>CRS</abbr> (this is not verified).
+     * Conversions, if needed, shall be done by the caller.
      *
      * @param  operation  the SQLMM operation to apply.
      * @param  other      the other geometry, or {@code null} if the operation 
requires only one geometry.
@@ -413,6 +453,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @throws UnsupportedOperationException if the operation cannot be 
performed with current implementation.
      * @throws ClassCastException if the operation can only be executed on 
some specific geometry subclasses
      *         (for example polylines) and the wrapped geometry is not of that 
class.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     protected Object operationSameCRS(SQLMM operation, GeometryWrapper other, 
Object argument) {
         throw new 
UnsupportedOperationException(Geometries.unsupported(operation.name()));
@@ -439,6 +480,7 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * @param  target  the desired type.
      * @return the converted geometry.
      * @throws IllegalArgumentException if the geometry cannot be converted to 
the specified type.
+     * @throws BackingStoreException if the operation failed because of a 
checked exception.
      */
     public GeometryWrapper toGeometryType(final GeometryType target) {
         final Class<?> type = factory().getGeometryClass(target);
@@ -451,85 +493,90 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
 
     /**
      * Transforms this geometry using the given coordinate operation.
-     * If the operation is {@code null}, then the geometry is returned 
unchanged.
-     * If the geometry uses a different CRS than the source CRS of the given 
operation
-     * and {@code validate} is {@code true},
-     * then a new operation to the target CRS will be automatically computed.
+     * If the geometry uses a different <abbr>CRS</abbr> than the source 
<abbr>CRS</abbr>
+     * of the given {@code operation} and if the {@code validate} argument is 
{@code true},
+     * then a new operation to the target <abbr>CRS</abbr> will be 
automatically computed.
      *
-     * <p>This method is preferred to {@link 
#transform(CoordinateReferenceSystem)}
-     * when possible because not all geometry libraries store the CRS of their 
objects.</p>
+     * <p>This method is preferred to the {@link 
#transform(CoordinateReferenceSystem)} method
+     * because not all geometry libraries store the <abbr>CRS</abbr> of their 
objects.</p>
      *
-     * @param  operation  the coordinate operation to apply, or {@code null}.
-     * @param  validate   whether to validate the operation source CRS.
+     * @param  operation  the coordinate operation to apply.
+     * @param  validate   whether to validate the operation source 
<abbr>CRS</abbr>.
      * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
      * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
-     * @throws FactoryException if transformation to the target CRS cannot be 
found.
+     * @throws FactoryException if transformation to the target 
<abbr>CRS</abbr> cannot be found.
      * @throws TransformException if the geometry cannot be transformed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
-    public GeometryWrapper transform(CoordinateOperation operation, boolean 
validate)
+    public GeometryWrapper transform(final CoordinateOperation operation, 
boolean validate)
             throws FactoryException, TransformException
     {
-        throw new 
UnsupportedOperationException(Geometries.unsupported("transform"));
+        MathTransform transform = operation.getMathTransform();
+        if (validate && crs != null) {
+            CoordinateOperation step = CRS.findOperation(crs, 
operation.getSourceCRS(), null);
+            transform = MathTransforms.concatenate(step.getMathTransform(), 
transform);
+        }
+        final GeometryWrapper wrapper = transform(transform);
+        wrapper.setCoordinateReferenceSystem(operation.getTargetCRS());
+        return wrapper;
     }
 
     /**
      * Transforms this geometry using the given transform.
-     * If the transform is {@code null}, then the geometry is returned 
unchanged.
-     * Otherwise, a new geometry is returned without CRS.
+     * If the transform is identity, then the geometry is returned unchanged.
+     * Otherwise, a new geometry is returned without <abbr>CRS</abbr>.
      *
-     * @param  transform  the math transform to apply, or {@code null}.
+     * @param  transform  the math transform to apply.
      * @return the transformed geometry (may be the same geometry instance, 
but never {@code null}).
      * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
      * @throws TransformException if the geometry cannot be transformed.
+     * @throws FactoryException if a problem happened while setting the 
<abbr>CRS</abbr>.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      */
-    public GeometryWrapper transform(MathTransform transform) throws 
TransformException {
-        if (transform == null || transform.isIdentity()) {
+    public GeometryWrapper transform(final MathTransform transform)
+            throws FactoryException, TransformException
+    {
+        if (transform.isIdentity()) {
             return this;
         }
         throw new 
UnsupportedOperationException(Geometries.unsupported("transform"));
     }
 
     /**
-     * Transforms this geometry to the specified Coordinate Reference System 
(CRS).
-     * If the given CRS is null, then the geometry is returned unchanged.
-     * If this geometry has no Coordinate Reference System, a {@link 
TransformException} is thrown.
+     * Transforms this geometry to the specified Coordinate Reference System 
(<abbr>CRS</abbr>).
+     * If the given <abbr>CRS</abbr> is {@code null} or the same as the 
current <abbr>CRS</abbr>,
+     * or if the geometry has no <abbr>CRS</abbr>, then this wrapper is 
returned unchanged.
      *
-     * <p>Consider using {@link #transform(CoordinateOperation, boolean)} 
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>
+     * <p>Consider using {@link #transform(CoordinateOperation, boolean)} 
instead of this method,
+     * for performance reasons and because not all geometry libraries 
associate <abbr>CRS</abbr>
+     * with their geometric objects.</p>
      *
      * @param  targetCRS  the target coordinate reference system, or {@code 
null}.
      * @return the transformed geometry (may be the same geometry but never 
{@code null}).
      * @throws UnsupportedOperationException if this operation is not 
supported for current implementation.
      * @throws TransformException if the given geometry has no CRS or cannot 
be transformed.
+     * @throws BackingStoreException if the operation failed because of 
another checked exception.
      *
      * @see #getCoordinateReferenceSystem()
      */
     @Override
     public GeometryWrapper transform(final CoordinateReferenceSystem 
targetCRS) throws TransformException {
-        if (targetCRS == null) {
+        if (targetCRS == null || targetCRS == crs || crs == null) {
             return this;
         }
-        throw new 
UnsupportedOperationException(Geometries.unsupported("transform"));
+        try {
+            return transform(CRS.findOperation(crs, targetCRS, null), false);
+        } catch (FactoryException e) {
+            /*
+             * We wrap that exception because `Geometry.transform(…)` does not 
declare `FactoryException`.
+             * We may revisit in a future version if `Geometry.transform(…)` 
method declaration is updated.
+             */
+            throw new TransformException(e);
+        }
     }
 
     /**
-     * Returns {@code true} if the given geometry use the same CRS as this 
geometry, or conservatively
-     * returns {@code false} in case of doubt. This method should perform only 
a cheap test; it is used
-     * as a way to filter rapidly if {@link 
#transform(CoordinateReferenceSystem)} needs to be invoked.
-     * If this method wrongly returned {@code false}, the {@code transform(…)} 
method will return the
-     * geometry unchanged anyway.
-     *
-     * <p>If both CRS are undefined (null), then they are considered the 
same.</p>
-     *
-     * @param  other  the second geometry.
-     * @return {@code true} if the two geometries use equivalent CRS or if the 
CRS is undefined on both side,
-     *         or {@code false} in case of doubt.
-     */
-    public abstract boolean isSameCRS(GeometryWrapper other);
-
-    /**
-     * Formats the wrapped geometry in Well Known Text (WKT).
+     * Formats the wrapped geometry in Well Known Text (<abbr>WKT</abbr>) 
format.
      * If the geometry contains curves, then the {@code flatness} parameter 
specifies the maximum distance that
      * the line segments used in the Well Known Text are allowed to deviate 
from any point on the original curve.
      * This parameter is ignored if the geometry does not contain curves.
@@ -559,27 +606,26 @@ public abstract class GeometryWrapper extends 
AbstractGeometry implements Geomet
      * for reducing the risk of compilation failures during the upcoming 
revision of GeoAPI interfaces since
      * some of those methods will be removed.
      */
-    @Deprecated public final Geometry       getMbRegion()                      
       {throw new UnsupportedOperationException();}
-    @Deprecated public final DirectPosition getRepresentativePoint()           
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Boundary       getBoundary()                      
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Complex        getClosure()                       
       {throw new UnsupportedOperationException();}
-    @Deprecated public final boolean        isSimple()                         
       {throw new UnsupportedOperationException();}
-    @Deprecated public final boolean        isCycle()                          
       {throw new UnsupportedOperationException();}
-    @Deprecated public final double         distance(Geometry geometry)        
       {throw new UnsupportedOperationException();}
-    @Deprecated public final int            getDimension(DirectPosition point) 
       {throw new UnsupportedOperationException();}
-    @Deprecated public final int            getCoordinateDimension()           
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Set<Complex>   getMaximalComplex()                
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Geometry       getConvexHull()                    
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Geometry       getBuffer(double distance)         
       {throw new UnsupportedOperationException();}
-    @Deprecated public final Geometry       clone() throws 
CloneNotSupportedException {throw new CloneNotSupportedException();}
-    @Deprecated public final boolean        contains(TransfiniteSet pointSet)  
       {throw new UnsupportedOperationException();}
-    @Deprecated public final boolean        contains(DirectPosition point)     
       {throw new UnsupportedOperationException();}
-    @Deprecated public final boolean        intersects(TransfiniteSet 
pointSet)       {throw new UnsupportedOperationException();}
-    @Deprecated public final boolean        equals(TransfiniteSet pointSet)    
       {throw new UnsupportedOperationException();}
-    @Deprecated public final TransfiniteSet union(TransfiniteSet pointSet)     
       {throw new UnsupportedOperationException();}
-    @Deprecated public final TransfiniteSet intersection(TransfiniteSet 
pointSet)     {throw new UnsupportedOperationException();}
-    @Deprecated public final TransfiniteSet difference(TransfiniteSet 
pointSet)       {throw new UnsupportedOperationException();}
-    @Deprecated public final TransfiniteSet symmetricDifference(TransfiniteSet 
ps)    {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Geometry       getMbRegion()            
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final DirectPosition getRepresentativePoint() 
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Boundary       getBoundary()            
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Complex        getClosure()             
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final boolean        isSimple()               
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final boolean        isCycle()                
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final double         distance(Geometry 
geometry)               {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final int            
getDimension(DirectPosition point)        {throw new 
UnsupportedOperationException();}
+    @Deprecated @Override public final Set<Complex>   getMaximalComplex()      
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Geometry       getConvexHull()          
                 {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Geometry       getBuffer(double 
distance)                {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final Geometry       clone() throws 
CloneNotSupportedException {throw new CloneNotSupportedException();}
+    @Deprecated @Override public final boolean        contains(TransfiniteSet 
pointSet)         {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final boolean        contains(DirectPosition 
point)            {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final boolean        
intersects(TransfiniteSet pointSet)       {throw new 
UnsupportedOperationException();}
+    @Deprecated @Override public final boolean        equals(TransfiniteSet 
pointSet)           {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final TransfiniteSet union(TransfiniteSet 
pointSet)            {throw new UnsupportedOperationException();}
+    @Deprecated @Override public final TransfiniteSet 
intersection(TransfiniteSet pointSet)     {throw new 
UnsupportedOperationException();}
+    @Deprecated @Override public final TransfiniteSet 
difference(TransfiniteSet pointSet)       {throw new 
UnsupportedOperationException();}
+    @Deprecated @Override public final TransfiniteSet 
symmetricDifference(TransfiniteSet ps)    {throw new 
UnsupportedOperationException();}
 
     /**
      * Returns {@code true} if the given object is a wrapper of the same class
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
index f2fb605459..f956791357 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/esri/Wrapper.java
@@ -39,7 +39,6 @@ import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.wrapper.Geometries;
-import org.apache.sis.geometry.wrapper.GeometryWithCRS;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import org.apache.sis.util.Debug;
@@ -55,7 +54,7 @@ import org.opengis.filter.SpatialOperatorName;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
  */
-final class Wrapper extends GeometryWithCRS {
+final class Wrapper extends GeometryWrapper {
     /**
      * The wrapped implementation.
      */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Factory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Factory.java
index 29b53f6ea6..54111c7fbd 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Factory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Factory.java
@@ -183,7 +183,7 @@ public final class Factory extends Geometries<Shape> {
      */
     @Override
     public Shape createMultiPoint(boolean isFloat, Dimensions dimensions, 
DoubleBuffer coordinates) {
-        throw new UnsupportedOperationException();
+        throw new 
UnsupportedImplementationException(unsupported("createMultiPoint"));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/PointWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/PointWrapper.java
index 43fe93c380..da3ce14fc3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/PointWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/PointWrapper.java
@@ -27,7 +27,6 @@ import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.wrapper.Geometries;
-import org.apache.sis.geometry.wrapper.GeometryWithCRS;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import org.apache.sis.util.Debug;
@@ -42,7 +41,7 @@ import org.opengis.filter.SpatialOperatorName;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class PointWrapper extends GeometryWithCRS {
+final class PointWrapper extends GeometryWrapper {
     /**
      * The wrapped implementation.
      */
@@ -176,19 +175,19 @@ final class PointWrapper extends GeometryWithCRS {
             case ST_Envelope:   return getEnvelope();
             case ST_Boundary: {
                 if (point instanceof Point) {
-                    final Point p = (Point) point;
-                    final Rectangle r = new Rectangle();
+                    final var p = (Point) point;
+                    final var r = new Rectangle();
                     r.x = p.x;
                     r.y = p.y;
                     return r;
                 } else if (point instanceof Point2D.Float) {
-                    final Point2D.Float p = (Point2D.Float) point;
-                    final Rectangle2D.Float r = new Rectangle2D.Float();
+                    final var p = (Point2D.Float) point;
+                    final var r = new Rectangle2D.Float();
                     r.x = p.x;
                     r.y = p.y;
                     return r;
                 } else {
-                    final Rectangle2D.Double r = new Rectangle2D.Double();
+                    final var r = new Rectangle2D.Double();
                     r.x = point.getX();
                     r.y = point.getY();
                     return r;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Wrapper.java
index 08bcc9eddb..f71187ce1d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/j2d/Wrapper.java
@@ -31,7 +31,6 @@ import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.wrapper.Geometries;
-import org.apache.sis.geometry.wrapper.GeometryWithCRS;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import org.apache.sis.referencing.privy.ShapeUtilities;
@@ -50,7 +49,7 @@ import org.opengis.filter.SpatialOperatorName;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
  */
-final class Wrapper extends GeometryWithCRS {
+final class Wrapper extends GeometryWrapper {
     /**
      * The wrapped implementation.
      */
@@ -166,8 +165,9 @@ final class Wrapper extends GeometryWithCRS {
         boolean lineTo = false;
 add:    for (;;) {
             if (next instanceof Point2D) {
-                final double x = ((Point2D) next).getX();
-                final double y = ((Point2D) next).getY();
+                final var p = (Point2D) next;
+                final double x = p.getX();
+                final double y = p.getY();
                 if (Double.isNaN(x) || Double.isNaN(y)) {
                     lineTo = false;
                 } else if (lineTo) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Factory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Factory.java
index 378a434ae8..2ef54b6da2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Factory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Factory.java
@@ -39,6 +39,7 @@ import org.locationtech.jts.geom.Polygon;
 import org.locationtech.jts.io.ParseException;
 import org.locationtech.jts.io.WKBReader;
 import org.locationtech.jts.io.WKTReader;
+import org.opengis.util.FactoryException;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.geometry.wrapper.Capability;
 import org.apache.sis.geometry.wrapper.Dimensions;
@@ -48,6 +49,7 @@ import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.BackingStoreException;
 
 
 /**
@@ -144,13 +146,14 @@ public final class Factory extends Geometries<Geometry> {
      * @param  geometry  the geometry instance to wrap (can be {@code null}).
      * @return a wrapper for the given geometry implementation, or {@code 
null}.
      * @throws ClassCastException if the given geometry is not an instance of 
valid type.
+     * @throws BackingStoreException if the <abbr>CRS</abbr> cannot be created 
from the <abbr>SRID</abbr> code.
      */
     @Override
     public GeometryWrapper castOrWrap(final Object geometry) {
         if (geometry == null || geometry instanceof Wrapper) {
             return (Wrapper) geometry;
         } else {
-            return new Wrapper((Geometry) geometry);
+            return createWrapper((Geometry) geometry);
         }
     }
 
@@ -159,10 +162,15 @@ public final class Factory extends Geometries<Geometry> {
      *
      * @param  geometry  the geometry to wrap.
      * @return wrapper for the given geometry.
+     * @throws BackingStoreException if the <abbr>CRS</abbr> cannot be created 
from the <abbr>SRID</abbr> code.
      */
     @Override
     protected GeometryWrapper createWrapper(final Geometry geometry) {
-        return new Wrapper(geometry);
+        try {
+            return new Wrapper(geometry);
+        } catch (FactoryException e) {
+            throw new BackingStoreException(e);
+        }
     }
 
     /**
@@ -423,6 +431,7 @@ public final class Factory extends Geometries<Geometry> {
      * @param  geometries  the polygons or linear rings to put in a 
multi-polygons.
      * @throws ClassCastException if an element in the array is not a JTS 
geometry.
      * @throws IllegalArgumentException if an element is a non-closed linear 
string.
+     * @throws BackingStoreException if the <abbr>CRS</abbr> cannot be created 
from the <abbr>SRID</abbr> code.
      */
     @Override
     public GeometryWrapper createMultiPolygon(final Object[] geometries) {
@@ -455,7 +464,7 @@ public final class Factory extends Geometries<Geometry> {
             }
             polygons[i] = polygon;
         }
-        return new Wrapper(factory(isFloat).createMultiPolygon(polygons));
+        return createWrapper(factory(isFloat).createMultiPolygon(polygons));
     }
 
     /**
@@ -476,6 +485,7 @@ public final class Factory extends Geometries<Geometry> {
      * @throws IllegalArgumentException if the given geometry type is not 
supported.
      * @throws ClassCastException if {@code components} is not an array or a 
collection of supported geometry components.
      * @throws ArrayStoreException if {@code components} is an array with 
invalid component type.
+     * @throws BackingStoreException if the <abbr>CRS</abbr> cannot be created 
from the <abbr>SRID</abbr> code.
      */
     @Override
     public GeometryWrapper createFromComponents(final GeometryType type, final 
Object components) {
@@ -535,7 +545,7 @@ public final class Factory extends Geometries<Geometry> {
                 throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1, 
type));
             }
         }
-        return new Wrapper(geometry);
+        return createWrapper(geometry);
     }
 
     /**
@@ -618,10 +628,11 @@ public final class Factory extends Geometries<Geometry> {
      *
      * @param  wkt  the Well Known Text to parse.
      * @return the geometry object for the given WKT.
-     * @throws ParseException if the WKT cannot be parsed.
+     * @throws ParseException if the <abbr>WKT</abbr> cannot be parsed.
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be created from 
the <abbr>SRID</abbr> code.
      */
     @Override
-    public GeometryWrapper parseWKT(final String wkt) throws ParseException {
+    public GeometryWrapper parseWKT(final String wkt) throws ParseException, 
FactoryException {
         // WKTReader(GeometryFactory) constructor is cheap.
         final WKTReader reader = new WKTReader(factory);
         reader.setIsOldJtsCoordinateSyntaxAllowed(false);
@@ -634,10 +645,11 @@ public final class Factory extends Geometries<Geometry> {
      *
      * @param  data  the sequence of bytes to parse.
      * @return the geometry object for the given WKB.
-     * @throws ParseException if the WKB cannot be parsed.
+     * @throws ParseException if the <abbr>WKB</abbr> cannot be parsed.
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be created from 
the <abbr>SRID</abbr> code.
      */
     @Override
-    public GeometryWrapper parseWKB(final ByteBuffer data) throws 
ParseException {
+    public GeometryWrapper parseWKB(final ByteBuffer data) throws 
ParseException, FactoryException {
         byte[] array;
         if (data.hasArray()) {
             /*
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
index 810e741785..114d41572b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java
@@ -74,43 +74,6 @@ public final class JTS extends Static {
     private JTS() {
     }
 
-    /**
-     * Returns {@code true} if the two geometries use the same CRS, based on 
very cheap comparison.
-     * A value of {@code false} does not necessarily means that the CRS are 
different, but it means
-     * that a more expensive comparison is required. If CRS are specified by 
SRID codes, then this
-     * method assumes that the two SRID codes are defined by the same 
authority (e.g. EPSG).
-     *
-     * <p>If both CRS are undefined (null), then they are considered the 
same.</p>
-     *
-     * @param  first   the first geometry.
-     * @param  second  the second geometry.
-     * @return {@code true} if the two geometries use equivalent CRS, or 
{@code false} in case of doubt.
-     */
-    static boolean isSameCRS(final Geometry first, final Geometry second) {
-        final int id1 =  first.getSRID();
-        final int id2 = second.getSRID();
-        if ((id1 | id2) != 0) {
-            return id1 == id2;
-        }
-        /*
-         * Identity comparison is often sufficient since all geometries 
typically share the same CRS.
-         * If they are not the same instance, a more expensive 
`equalsIgnoreMetadata(…)` method here
-         * would probably duplicate the work done later by the 
`transform(Geometry, …)` method.
-         */
-        Object c1 = first.getUserData();
-        if (c1 != null && !(c1 instanceof CoordinateReferenceSystem)) {
-            c1 = (c1 instanceof Map<?,?>) ? ((Map<?,?>) c1).get(CRS_KEY) : 
null;
-        }
-        Object c2 = second.getUserData();
-        if (c1 == c2) {
-            return true;                // Quick check for common case.
-        }
-        if (c2 != null && !(c2 instanceof CoordinateReferenceSystem)) {
-            c2 = (c2 instanceof Map<?,?>) ? ((Map<?,?>) c2).get(CRS_KEY) : 
null;
-        }
-        return c1 == c2;
-    }
-
     /**
      * Gets the Coordinate Reference System (CRS) from the given geometry.
      * This method expects the CRS to be stored in one the following ways:
@@ -202,7 +165,7 @@ 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.
+     * This method does not verify the <abbr>CRS</abbr> of the given geometry.
      *
      * @param  sourceCRS       the CRS of source coordinates.
      * @param  targetCRS       the CRS of target coordinates.
@@ -233,9 +196,9 @@ public final class JTS extends Static {
     }
 
     /**
-     * Transforms the given geometry to the specified Coordinate Reference 
System (CRS).
-     * If the given CRS or the given geometry is null or is the same as 
current CRS,
-     * then the geometry is returned unchanged.
+     * Transforms the given geometry to the specified Coordinate Reference 
System (<abbr>CRS</abbr>).
+     * If the given geometry is {@code null}, or if the given <abbr>CRS</abbr> 
is {@code null} or the
+     * same as the current geometry <abbr>CRS</abbr>, then the given geometry 
is returned unchanged.
      * If the geometry has no Coordinate Reference System, then the geometry 
is returned unchanged.
      *
      * <p><b>This operation may be slow!</b>
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index 6e2baa24a1..520150bf1f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@ -58,7 +58,6 @@ import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Debug;
-import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.filter.sqlmm.SQLMM;
 import static org.apache.sis.geometry.wrapper.GeometryType.POINT;
@@ -83,20 +82,25 @@ final class Wrapper extends GeometryWrapper {
 
     /**
      * Creates a new wrapper around the given geometry.
+     *
+     * @param  geometry  the geometry to wrap.
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be created from 
the <abbr>SRID</abbr> code.
      */
-    Wrapper(final Geometry geometry) {
+    Wrapper(final Geometry geometry) throws FactoryException {
         this.geometry = geometry;
+        crs = JTS.getCoordinateReferenceSystem(geometry);
     }
 
     /**
-     * Returns the given geometry in new wrapper,
-     * or {@code this} if {@code g} is same as current geometry.
+     * Creates a new wrapper with the same <abbr>CRS</abbr> than the given 
wrapper.
      *
-     * @param  result  the geometry computed by a JTS operation.
-     * @return wrapper for the given geometry. May be {@code this}.
+     * @param  source    the source wrapper from which is derived the geometry.
+     * @param  geometry  the geometry to wrap.
      */
-    private Wrapper rewrap(final Geometry result) {
-        return (result != geometry) ? new Wrapper(result) : this;
+    private Wrapper(final Wrapper source, final Geometry geometry) {
+        this.geometry = geometry;
+        this.crs = source.crs;
+        JTS.copyMetadata(source.geometry, geometry);
     }
 
     /**
@@ -127,21 +131,6 @@ final class Wrapper extends GeometryWrapper {
         return (srid != 0) ? OptionalInt.of(srid) : OptionalInt.empty();
     }
 
-    /**
-     * Returns the geometry coordinate reference system, or {@code null} if 
none.
-     *
-     * @return the coordinate reference system, or {@code null} if none.
-     * @throws BackingStoreException if the CRS cannot be created from the 
SRID code.
-     */
-    @Override
-    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
-        try {
-            return JTS.getCoordinateReferenceSystem(geometry);
-        } catch (FactoryException e) {
-            throw new BackingStoreException(e);
-        }
-    }
-
     /**
      * Sets the coordinate reference system. This method overwrites any 
previous user object.
      * This is okay for the context in which Apache SIS uses this method, 
which is only for
@@ -149,10 +138,18 @@ final class Wrapper extends GeometryWrapper {
      */
     @Override
     public void setCoordinateReferenceSystem(final CoordinateReferenceSystem 
crs) {
-        ArgumentChecks.ensureDimensionMatches("crs", 
getCoordinatesDimension(geometry), crs);
+        super.setCoordinateReferenceSystem(crs);
         JTS.setCoordinateReferenceSystem(geometry, crs);
     }
 
+    /**
+     * Returns the dimension of the coordinates that define this geometry.
+     */
+    @Override
+    public int getCoordinateDimension() {
+        return getCoordinatesDimension(geometry);
+    }
+
     /**
      * Gets the number of dimensions of geometry vertex (sequence of 
coordinate tuples), which can be 2 or 3.
      * Note that this is different than the {@linkplain 
Geometry#getDimension() geometry topological dimension},
@@ -215,6 +212,7 @@ final class Wrapper extends GeometryWrapper {
     @Override
     public DirectPosition getCentroid() {
         final Coordinate c = geometry.getCentroid().getCoordinate();
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final CoordinateReferenceSystem crs = getCoordinateReferenceSystem();
         if (crs == null) {
             final double z = c.getZ();
@@ -555,8 +553,7 @@ add:    for (Geometry next = geometry;;) {
         if (!getGeometryClass(target).isInstance(geometry)) {
             final Geometry result = convert(target);
             if (result != geometry) {
-                JTS.copyMetadata(geometry, result);
-                return new Wrapper(result);
+                return new Wrapper(this, result);
             }
         }
         return this;
@@ -774,34 +771,32 @@ add:    for (Geometry next = geometry;;) {
      * @throws TransformException if the geometry cannot be transformed.
      */
     @Override
-    public GeometryWrapper transform(final MathTransform transform) throws 
TransformException {
+    public GeometryWrapper transform(final MathTransform transform) throws 
FactoryException, TransformException {
         return rewrap(JTS.transform(geometry, transform));
     }
 
     /**
-     * Returns a view over the JTS geometry as a Java2D shape. Changes in the 
JTS geometry
-     * after this method call may be reflected in the returned shape in an 
unspecified way.
+     * Returns the given geometry in a new wrapper, or {@code this} if {@code 
result} is same as current geometry.
+     * If a new wrapper is created, then its <abbr>CRS</abbr> will be inferred 
from the geometry <abbr>SRID</abbr>
+     * or user properties.
      *
-     * @return a view over the geometry as a Java2D shape.
+     * @param  result  the geometry computed by a JTS operation.
+     * @return wrapper for the given geometry. May be {@code this}.
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be created from 
the <abbr>SRID</abbr> code.
      */
-    @Override
-    public Shape toJava2D() {
-        return JTS.asShape(geometry);
+    private Wrapper rewrap(final Geometry result) throws FactoryException {
+        return (result == geometry) ? this : new Wrapper(result);
     }
 
     /**
-     * Returns {@code true} if the given geometry use the same CRS as this 
geometry, or conservatively
-     * returns {@code false} in case of doubt. This method should perform only 
a cheap test; it is used
-     * as a way to filter rapidly if {@link 
#transform(CoordinateReferenceSystem)} needs to be invoked.
+     * Returns a view over the JTS geometry as a Java2D shape. Changes in the 
JTS geometry
+     * after this method call may be reflected in the returned shape in an 
unspecified way.
      *
-     * @param  other  the second geometry.
-     * @return {@code true} if the two geometries use equivalent CRS or if the 
CRS is undefined on both side,
-     *         or {@code false} in case of doubt.
-     * @throws ClassCastException if the given wrapper is not for the same 
geometry library.
+     * @return a view over the geometry as a Java2D shape.
      */
     @Override
-    public boolean isSameCRS(final GeometryWrapper other) {
-        return JTS.isSameCRS(geometry, ((Wrapper) other).geometry);
+    public Shape toJava2D() {
+        return JTS.asShape(geometry);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
index 8aec3d6a9e..2998c4ecdd 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java
@@ -95,7 +95,7 @@ public final class JTSTest extends TestCase {
              * Test setting a 2D CRS on a 2 dimensional geometry.
              */
             final Geometry geometry = factory.createPoint(new CoordinateXY(5, 
6));
-            final GeometryWrapper wrapper = Geometries.wrap(geometry).get();
+            final GeometryWrapper wrapper = 
Geometries.wrap(geometry).orElseThrow();
             wrapper.setCoordinateReferenceSystem(crs2D);
             assertEquals(crs2D, wrapper.getCoordinateReferenceSystem());
         }
@@ -150,7 +150,7 @@ public final class JTSTest extends TestCase {
              */
             final CoordinateReferenceSystem crs = CommonCRS.WGS84.geographic();
             final Geometry geometry = factory.createPoint(new CoordinateXY(5, 
6));
-            final GeometryWrapper wrapper = Geometries.wrap(geometry).get();
+            final GeometryWrapper wrapper = 
Geometries.wrap(geometry).orElseThrow();
             wrapper.setCoordinateReferenceSystem(crs);
             final GeneralEnvelope envelope = wrapper.getEnvelope();
             assertEquals(crs, envelope.getCoordinateReferenceSystem());
@@ -166,7 +166,7 @@ public final class JTSTest extends TestCase {
              */
             final CoordinateReferenceSystem crs = 
CommonCRS.WGS84.geographic3D();
             final Geometry geometry = factory.createPoint(new Coordinate(5, 6, 
7));
-            final GeometryWrapper wrapper = Geometries.wrap(geometry).get();
+            final GeometryWrapper wrapper = 
Geometries.wrap(geometry).orElseThrow();
             wrapper.setCoordinateReferenceSystem(crs);
             final GeneralEnvelope envelope = wrapper.getEnvelope();
             assertEquals(crs, envelope.getCoordinateReferenceSystem());
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
index 9e13d21269..5963cbc2b3 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java
@@ -26,7 +26,6 @@ import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
-import org.apache.sis.geometry.wrapper.GeometryWithCRS;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.setup.GeometryLibrary;
@@ -109,11 +108,10 @@ public final class GeometryGetterTest extends TestCase {
         assertEquals(GF.createPoint(42.2, 43.3), geometry);
         final GeometryWrapper wrapper = 
Geometries.factory(library).castOrWrap(geometry);
         /*
-         * If the wrapper is an instance of `GeometryWithCRS`, then the CRS is 
stored
-         * with the wrapper instead of the geometry implementation. In such 
case, the
-         * CRS is lost on `GeometryWrapper.geometry()` and cannot be tested.
+         * The following assertion can be executed only with library that 
store the CRS
+         * in the geometry object itself rather than as a field of the 
geometry wrapper.
          */
-        if (!(wrapper instanceof GeometryWithCRS)) {
+        if (library == GeometryLibrary.JTS) {
             assertSame(HardCodedCRS.WGS84, 
wrapper.getCoordinateReferenceSystem());
         }
     }
diff --git 
a/incubator/src/org.apache.sis.storage.geopackage/test/org/apache/sis/storage/geopackage/GpkgStoreTest.java
 
b/incubator/src/org.apache.sis.storage.geopackage/test/org/apache/sis/storage/geopackage/GpkgStoreTest.java
index 15eb2bd9b1..6ed52ec5ed 100644
--- 
a/incubator/src/org.apache.sis.storage.geopackage/test/org/apache/sis/storage/geopackage/GpkgStoreTest.java
+++ 
b/incubator/src/org.apache.sis.storage.geopackage/test/org/apache/sis/storage/geopackage/GpkgStoreTest.java
@@ -281,7 +281,7 @@ public final class GpkgStoreTest {
         assertEquals(1, features.size());
         final Feature feature = features.get(0);
 
-        GeometryWrapper geometry = 
Geometries.wrap(feature.getPropertyValue("sis:geometry")).get();
+        GeometryWrapper geometry = Geometries.wrap(feature).orElseThrow();
         assertEquals(expectedCRS, 
IdentifiedObjects.getIdentifierOrName(geometry.getCoordinateReferenceSystem()));
         assertEquals(expectedWKT, geometry.formatWKT(1));
         assertEquals(1, feature.getPropertyValue("fid"));

Reply via email to