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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 76bdc396c1 Resole the problem documented in the `TODO` comment of 
`GridDerivation`: When a wraparound may happen, the computation of envelope 
unions need to be done between compatible envelopes (computed with the same 
wraparound shift).
76bdc396c1 is described below

commit 76bdc396c1aea5b5505a47be512559b9f584454b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Aug 3 17:21:05 2025 +0200

    Resole the problem documented in the `TODO` comment of `GridDerivation`:
    When a wraparound may happen, the computation of envelope unions need to be 
done between compatible envelopes (computed with the same wraparound shift).
---
 .../apache/sis/coverage/grid/GridDerivation.java   |  32 ++--
 .../org/apache/sis/coverage/grid/GridExtent.java   |   9 +-
 .../main/org/apache/sis/geometry/Envelopes.java    | 102 ++++++++----
 .../apache/sis/geometry/WraparoundInEnvelope.java  | 171 ++++++++++++++++++++-
 .../main/org/apache/sis/io/wkt/Formatter.java      |   2 +-
 .../sis/parameter/DefaultParameterDescriptor.java  |   2 +
 .../main/org/apache/sis/parameter/Verifier.java    |  79 ++++++----
 .../referencing/operation/provider/Wraparound.java |   2 +-
 .../operation/transform/MolodenskyTransform.java   |   2 +-
 .../operation/transform/WraparoundTransform.java   |   4 +-
 .../org/apache/sis/geometry/EnvelopesTest.java     |  12 +-
 11 files changed, 331 insertions(+), 86 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
index c3087896bc..a1932913f5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridDerivation.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.coverage.grid;
 
+import java.util.Map;
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.Objects;
@@ -40,6 +41,7 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.WraparoundAdjustment;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.CharSequences;
@@ -612,26 +614,30 @@ public class GridDerivation {
                 }
                 nowraparound = finder.gridToGrid();
                 /*
-                 * Converts the grid extent of the area of interest to the 
grid coordinates of the base.
-                 * We may get one or two envelopes, depending on whether there 
is a longitude wraparound.
-                 * If the area of interest has less dimensions than the base 
grid, we may need to compute
-                 * more envelopes for enclosing the full span of dimensions 
that are not in `areaOfInterest`.
+                 * At this point, the transform between the coordinate systems 
of the two grids is known.
+                 * The `gridToGrid` transform is the main one. If the user did 
not specified an AOI for
+                 * all dimensions, then `gridToGrid` is for low coordinates 
and `gridToGridHigh` is for
+                 * high coordinates.
                  */
                 final GeneralEnvelope[] envelopes;
                 if (gridToGrid.isIdentity() && (gridToGridHigh == null || 
gridToGridHigh.isIdentity())) {
                     envelopes = new GeneralEnvelope[] 
{domain.toEnvelope(tight)};
                 } else {
-                    envelopes = domain.toEnvelopes(gridToGrid, tight, 
nowraparound, null);
+                    /*
+                     * Converts the grid extent of the area of interest to the 
grid coordinates of the base.
+                     * We may get one or two envelopes, depending on whether 
there is a longitude wraparound.
+                     * If the area of interest has less dimensions than the 
base grid, we may need to compute
+                     * more envelopes for enclosing the full span of 
dimensions that are not in `areaOfInterest`.
+                     */
+                    final Map<Parameters, GeneralEnvelope> transformed =
+                            domain.toEnvelopes(gridToGrid, tight, 
nowraparound, null);
                     if (gridToGridHigh != null) {
-                        final GeneralEnvelope[] high = 
domain.toEnvelopes(gridToGridHigh, tight, nowraparoundHigh, null);
-                        for (int i = Math.min(envelopes.length, high.length); 
--i >= 0;) {
-                            /*
-                             * TODO: actually, we have no guarantee that the 
envelopes at the same index match.
-                             * We need to find a more reliable algorithm, 
maybe by checking intersection first.
-                             */
-                            envelopes[i].add(high[i]);
-                        }
+                        domain.toEnvelopes(gridToGridHigh, tight, 
nowraparoundHigh, null).forEach((key, value) -> {
+                            GeneralEnvelope previous = 
transformed.putIfAbsent(key, value);
+                            if (previous != null) previous.add(value);
+                        });
                     }
+                    envelopes = 
transformed.values().toArray(GeneralEnvelope[]::new);
                 }
                 setBaseExtentClipped(tight, envelopes);
                 if (baseExtent != base.extent && baseExtent.equals(domain)) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index 072ed3aada..5afcbfa8b5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
@@ -62,6 +62,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.system.Modules;
@@ -1288,12 +1289,12 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      * @see #GridExtent(AbstractEnvelope, GridRoundingMode, int[], GridExtent, 
int[])
      * @see GridGeometry#getEnvelope(CoordinateReferenceSystem)
      */
-    final GeneralEnvelope[] toEnvelopes(final MathTransform gridToCRS, final 
boolean isCenter,
-                                        final MathTransform specified, final 
Envelope fallback)
+    final Map<Parameters, GeneralEnvelope> toEnvelopes(final MathTransform 
gridToCRS, final boolean isCenter,
+                                                       final MathTransform 
specified, final Envelope fallback)
             throws TransformException
     {
-        final GeneralEnvelope[] envelopes = Envelopes.wraparound(gridToCRS, 
toEnvelope(isCenter));
-        for (final GeneralEnvelope envelope : envelopes) {
+        final Map<Parameters, GeneralEnvelope> envelopes = 
Envelopes.transformWithWraparound(gridToCRS, toEnvelope(isCenter));
+        for (final GeneralEnvelope envelope : envelopes.values()) {
             complete(envelope, specified, (specified == gridToCRS) == 
isCenter, fallback);
         }
         return envelopes;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
index 9c29373152..15776e3034 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
@@ -22,10 +22,10 @@ package org.apache.sis.geometry;
  * support Java2D (e.g. Android),  or applications that do not need it may 
want to avoid to
  * force installation of the Java2D module (e.g. JavaFX/SWT).
  */
+import java.util.Map;
 import java.util.Set;
-import java.util.List;
 import java.util.Optional;
-import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.ConcurrentModificationException;
 import java.util.logging.Logger;
 import java.time.Instant;
@@ -44,10 +44,12 @@ import 
org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.metadata.privy.ReferencingServices;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
 import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
-import org.apache.sis.metadata.privy.ReferencingServices;
+import org.apache.sis.referencing.operation.transform.WraparoundTransform;
 import org.apache.sis.referencing.privy.CoordinateOperations;
 import org.apache.sis.referencing.privy.DirectPositionView;
 import org.apache.sis.referencing.privy.TemporalAccessor;
@@ -368,8 +370,8 @@ public final class Envelopes extends Static {
     }
 
     /**
-     * Shared implementation of {@link #transform(MathTransform, Envelope)}
-     * and {@link #wraparound(MathTransform, Envelope)} public methods.
+     * Shared implementation of {@link #transform(MathTransform, Envelope)} and
+     * {@link #transformWithWraparound(MathTransform, Envelope)} public 
methods.
      * Offers also the opportunity to save the transformed center coordinates.
      *
      * @param  transform  the transform to use.
@@ -382,7 +384,7 @@ public final class Envelopes extends Static {
      * @return the transformed envelope. May be {@code null} if {@code 
results} was non-null.
      */
     private static GeneralEnvelope transform(final MathTransform transform, 
final Envelope envelope,
-            double[] targetPt, final List<GeneralEnvelope> results) throws 
TransformException
+            double[] targetPt, final Map<Parameters, GeneralEnvelope> results) 
throws TransformException
     {
         if (transform.isIdentity()) {
             /*
@@ -580,7 +582,8 @@ nextPoint:  for (int pointIndex = 0;;) {                // 
Break condition at th
              * `GridExtent` have algorithms for completing empty envelopes.
              */
             if (results != null) {
-                results.add(transformed);
+                final GeneralEnvelope e = results.putIfAbsent(wc.state(), 
transformed);
+                if (e != null) e.add(transformed);    // Should never happen, 
but let be safe.
                 transformed = null;
             }
         } while (wc.translate());
@@ -954,9 +957,8 @@ poles:  for (int i=0; i<dimension; i++) {
      *
      * <h4>Limitation</h4>
      * This method cannot handle the case where the envelope contains the 
North or South pole.
-     * Envelopes crossing the ±180° longitude are handled only if the given 
transform contains
-     * {@link 
org.apache.sis.referencing.operation.transform.WraparoundTransform} steps;
-     * this method does not add such steps itself.
+     * Furthermore, envelopes crossing the ±180° longitude are handled only if 
the given transform
+     * contains {@link WraparoundTransform} steps, as this method does not add 
such steps itself.
      * For a more robust envelope transformation, use {@link 
#transform(CoordinateOperation, Envelope)} instead.
      *
      * @param  transform  the transform to use.
@@ -977,38 +979,84 @@ poles:  for (int i=0; i<dimension; i++) {
 
     /**
      * Transforms potentially many times an envelope using the given math 
transform.
-     * If the given envelope is {@code null}, then this method returns an 
empty envelope.
-     * Otherwise, if the transform does not contain any
-     * {@link 
org.apache.sis.referencing.operation.transform.WraparoundTransform} step,
-     * then this method is equivalent to {@link #transform(MathTransform, 
Envelope)} returned in an array of length 1.
-     * Otherwise, this method returns many transformed envelopes where each 
envelope describes approximately the same region.
-     * If the envelope CRS is geographic, the many envelopes are the same 
envelope shifted by 360° of longitude.
-     * If the envelope CRS is projected, then the 360° shifts are applied 
before the map projection.
-     * It may result in very different coordinates.
+     * If the given envelope is {@code null}, then this method returns an 
empty map.
+     * Otherwise, if the given transform does not contain any {@link 
WraparoundTransform} step,
+     * then this method is equivalent to {@link #transform(MathTransform, 
Envelope)} returned in a singleton map.
+     * Otherwise, this method returns many transformed envelopes where each 
envelope describes approximately the
+     * same region. For example:
+     *
+     * <ul>
+     *   <li>If the envelope <abbr>CRS</abbr> is geographic,
+     *       the map values are the same envelope shifted by 360° of 
longitude.</li>
+     *   <li>If the envelope <abbr>CRS</abbr> is projected, then the 360° 
shifts are applied
+     *       before the map projection. It may result in very different 
coordinates.</li>
+     * </ul>
+     *
+     * The keys identify which translations were applied on wraparound axes 
for computing the associated envelope.
+     * For example, a key may identify an envelope computed with a shift of 
360° of longitude on some coordinates,
+     * and another key may identify the same envelope but computed with a 
shift of −360° of longitude.
+     * The content of those keys is implementation dependent and users should 
not rely on the exact parameters.
+     * The main contract is that, for envelopes computed with the same 
transform,
+     * the values that are associated with {@linkplain Object#equals(Object) 
equal} keys
+     * were computed with wraparounds applied in the same way (e.g. +360° 
versus −360°).
+     *
+     * <p><b>Example:</b> the union of two envelopes taking in account 
wraparounds can be computed as below:</p>
+     *
+     * {@snippet lang="java" :
+     *     MathTransform transform = ...;
+     *     Map<Parameters, GeneralEnvelope> result = 
Envelopes.transformWithWraparound(transform, envelope1);
+     *     Envelopes.transformWithWraparound(transform, 
envelope2).forEach((key, value) -> {
+     *         GeneralEnvelope previous = result.putIfAbsent(key, value);
+     *         if (previous != null) previous.add(value);   // Envelope union.
+     *     });
+     * }
+     *
+     * Note that the key may be {@code null} if the given transform
+     * does not contain any {@link WraparoundTransform} step.
      *
      * @param  transform  the transform to use.
      * @param  envelope   envelope to transform, or {@code null}. This 
envelope will not be modified.
-     * @return the transformed envelopes, or an empty array if {@code 
envelope} was null.
+     * @return the transformed envelopes, or an empty map if {@code envelope} 
was null.
      * @throws TransformException if a transform failed.
      *
      * @see #transform(MathTransform, Envelope)
-     * @see org.apache.sis.referencing.operation.transform.WraparoundTransform
+     * @see WraparoundTransform
      *
-     * @since 1.3
+     * @since 1.5
      */
-    public static GeneralEnvelope[] wraparound(final MathTransform transform, 
final Envelope envelope)
-            throws TransformException
+    public static Map<Parameters, GeneralEnvelope> transformWithWraparound(
+            final MathTransform transform, final Envelope envelope) throws 
TransformException
     {
         ArgumentChecks.ensureNonNull("transform", transform);
         if (envelope == null) {
-            return new GeneralEnvelope[0];
+            return Map.of();
         }
-        final var results = new ArrayList<GeneralEnvelope>(4);
+        final var results = new LinkedHashMap<Parameters, GeneralEnvelope>(4);
         final GeneralEnvelope transformed = transform(transform, envelope, 
null, results);
         if (results.isEmpty() && transformed != null) {
-            return new GeneralEnvelope[] {transformed};
+            results.put(null, transformed);
         }
-        return results.toArray(GeneralEnvelope[]::new);
+        return results;
+    }
+
+    /**
+     * Transforms potentially many times an envelope using the given math 
transform.
+     * If the given envelope is {@code null}, then this method returns an 
empty array.
+     *
+     * @param  transform  the transform to use.
+     * @param  envelope   envelope to transform, or {@code null}. This 
envelope will not be modified.
+     * @return the transformed envelopes, or an empty array if {@code 
envelope} was null.
+     * @throws TransformException if a transform failed.
+     *
+     * @since 1.3
+     *
+     * @deprecated Replaced by {@link #transformWithWraparound(MathTransform, 
Envelope)}.
+     */
+    @Deprecated(since = "1.5", forRemoval = true)
+    public static GeneralEnvelope[] wraparound(final MathTransform transform, 
final Envelope envelope)
+            throws TransformException
+    {
+        return transformWithWraparound(transform, 
envelope).values().toArray(GeneralEnvelope[]::new);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/WraparoundInEnvelope.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/WraparoundInEnvelope.java
index bfff4b5f29..9e22f41a24 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/WraparoundInEnvelope.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/WraparoundInEnvelope.java
@@ -16,10 +16,22 @@
  */
 package org.apache.sis.geometry;
 
+import java.util.List;
 import java.util.function.UnaryOperator;
+import org.opengis.parameter.GeneralParameterValue;
+import org.opengis.parameter.ParameterValue;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.operation.MathTransform;
-import org.apache.sis.referencing.operation.transform.WraparoundTransform;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.privy.Numerics;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.operation.provider.Wraparound;
+import org.apache.sis.referencing.operation.transform.WraparoundTransform;
 
 
 /**
@@ -74,12 +86,16 @@ final class WraparoundInEnvelope extends 
WraparoundTransform {
     /**
      * Number of cycles at the {@linkplain #sourceMedian} position. This is 
the minimum or maximum number
      * of cycles to remove to a coordinate, depending if that coordinate is 
before or after the median.
+     *
+     * <p>This value is an integer, but stored as a {@code double} for 
avoiding type conversions.
+     * This is initialized at construction time, then changed when {@link 
#translate()} is invoked.</p>
      */
     private double limit;
 
     /**
      * The minimum and maximum number of {@linkplain #period period}s that 
{@link #shift(double)} wanted
      * to add to the coordinate before to be constrained to the {@link #limit}.
+     * This value is an integer, but stored as a {@code double} for avoiding 
type conversions.
      */
     private double minCycles, maxCycles;
 
@@ -197,7 +213,7 @@ final class WraparoundInEnvelope extends 
WraparoundTransform {
             if (!Double.isFinite(transform.sourceMedian)) {
                 return transform;
             }
-            final WraparoundInEnvelope w = new WraparoundInEnvelope(transform);
+            final var w = new WraparoundInEnvelope(transform);
             if (wraparounds == null) {
                 wraparounds = new WraparoundInEnvelope[] {w};
             } else {
@@ -226,5 +242,156 @@ final class WraparoundInEnvelope extends 
WraparoundTransform {
             }
             return modified;
         }
+
+        /**
+         * Returns a snapshot of the state of the wraparound transform.
+         * This state can change when {@link #translate()} is invoked.
+         * It may be used as a key in a map.
+         *
+         * @return a snapshot of the state of the wraparound transform.
+         */
+        final Parameters state() {
+            State state = null;
+            if (wraparounds != null) {
+                for (final WraparoundInEnvelope tr : wraparounds) {
+                    state = new State(tr, state);
+                }
+            }
+            return state;
+        }
+    }
+
+    /**
+     * A semi-opaque object that describes the state of the wraparound 
transform.
+     * The parameters published by this object are not committed API and may 
change in any version.
+     * The current implementation is a linked list, but this list usually 
contains only one element.
+     *
+     * <p>The purpose of this class is to be used as keys in a hash map. 
Therefore, the only important
+     * methods are {@link #hashCode()} and {@link #equals(Object)}. The other 
methods are defined for
+     * compliance with the {@link Parameters} contract, but should not be 
used.</p>
+     */
+    private static final class State extends Parameters {
+        /** The group of parameters published by the public methods. */
+        private static volatile ParameterDescriptorGroup parameters;
+
+        /** Copy of a value from the enclosing wraparound transform. */
+        private final int wraparoundDimension;
+
+        /** Copies of values from the enclosing wraparound transform. */
+        private final double period, limit;
+
+        /** State of the wraparound transform executed before this one. */
+        private final State previous;
+
+        /** Creates a snapshot of the state of the given transforms. */
+        State(final WraparoundInEnvelope tr, final State previous) {
+            wraparoundDimension = tr.wraparoundDimension;
+            this.period   = tr.period;
+            this.limit    = tr.limit;
+            this.previous = previous;
+        }
+
+        /**
+         * Returns a hash code value for this state.
+         * It includes the hash code of previous {@code State} instances in 
the linked list.
+         */
+        @Override
+        public int hashCode() {
+            int hash = wraparoundDimension;
+            State s1 = this;
+            do hash = 37*hash + (31*Double.hashCode(s1.limit) + 
Double.hashCode(s1.period));
+            while ((s1 = s1.previous) != null);
+            return hash;
+        }
+
+        /**
+         * Compares this state with the given object for equality.
+         * The previous elements of the linked list are also compared.
+         */
+        @Override
+        public boolean equals(final Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (obj instanceof State) {
+                State s1 = this;
+                State s2 = (State) obj;
+                while (Numerics.equals(s1.limit,  s2.limit)  &&
+                       Numerics.equals(s1.period, s2.period) &&
+                       s1.wraparoundDimension == s2.wraparoundDimension)
+                {
+                    s1 = s1.previous;
+                    s2 = s2.previous;
+                    if (s1 == s2) return true;  // We are mostly interrested 
the case when both are null.
+                    if (s1 == null || s2 == null) break;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns the group of parameters published by the object.
+         * Provided for compliance with the interface contract, but
+         * not useful for the purpose of the {@code State} object.
+         */
+        @Override
+        public ParameterDescriptorGroup getDescriptor() {
+            ParameterDescriptorGroup p = parameters;
+            if (p == null) {
+                final var builder = new 
ParameterBuilder().setCodeSpace(Citations.SIS, "SIS");
+                final ParameterDescriptor<Double> shift = 
builder.addName("shift").create(Double.NaN, null);
+                parameters = p = builder.addName("Wraparound 
state").createGroup(Wraparound.WRAPAROUND_DIMENSION, shift);
+            }
+            return p;
+        }
+
+        /**
+         * Returns the values of all parameters defined by {@link 
#getDescriptor()}.
+         * Provided for compliance with the interface contract, but* not 
useful for
+         * the purpose of the {@code State} object.
+         */
+        @Override
+        public List<GeneralParameterValue> values() {
+            return List.of(parameter("wraparound_dim"), parameter("shift"));
+        }
+
+        /**
+         * Returns the value of the parameter of the given name.
+         * Provided for compliance with the interface contract,
+         * but* not useful for the purpose of {@code State}.
+         */
+        @Override
+        public ParameterValue<?> parameter(String name) {
+            switch (name) {
+                case "wraparound_dim": {
+                    ParameterValue<Integer> p = 
Wraparound.WRAPAROUND_DIMENSION.createValue();
+                    p.setValue(wraparoundDimension);
+                    return p;
+                }
+                case "shift": {
+                    var p = (ParameterValue<?>) 
getDescriptor().descriptors().get(1).createValue();
+                    p.setValue(period * limit);
+                    return p;
+                }
+                default: throw new ParameterNotFoundException(null, name);
+            }
+        }
+
+        /**
+         * Unsupported operation. Actually, we could return the parameters of 
a previous element
+         * of the linked list. But this is not implemented yet because we have 
no known usage.
+         */
+        @Override
+        public List<ParameterValueGroup> groups(String name) {
+            throw new ParameterNotFoundException(null, name);
+        }
+
+        /**
+         * Unsupported operation as this parameter group is unmodifiable.
+         */
+        @Override
+        public ParameterValueGroup addGroup(String name) {
+            throw new ParameterNotFoundException(null, name);
+        }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
index 25629c7250..513ac1491a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Formatter.java
@@ -1907,7 +1907,7 @@ public class Formatter implements Localized {
     }
 
     /**
-     * Adds a warning for for an exception that occurred while fetching an 
optional property.
+     * Adds a warning for an exception that occurred while fetching an 
optional property.
      *
      * @param e        the exception that occurred.
      * @param element  WKT keyword of the element where the exception occurred.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
index ed2c0ffc10..04612d322a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
@@ -395,6 +395,7 @@ public class DefaultParameterDescriptor<T> extends 
AbstractParameterDescriptor i
     @Override
     @SuppressWarnings("unchecked")
     public Comparable<T> getMinimumValue() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Range<?> valueDomain = this.valueDomain;
         return (valueDomain != null && valueDomain.getElementType() == 
valueClass)
                ? (Comparable<T>) valueDomain.getMinValue() : null;
@@ -414,6 +415,7 @@ public class DefaultParameterDescriptor<T> extends 
AbstractParameterDescriptor i
     @Override
     @SuppressWarnings("unchecked")
     public Comparable<T> getMaximumValue() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Range<?> valueDomain = this.valueDomain;
         return (valueDomain != null && valueDomain.getElementType() == 
valueClass)
                ? (Comparable<T>) valueDomain.getMaxValue() : null;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
index 93f911f797..3f7191b59f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
@@ -38,6 +38,7 @@ import org.apache.sis.measure.Units;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.IndexedResourceBundle;
 
 
 /**
@@ -59,11 +60,6 @@ final class Verifier {
      */
     private final boolean internal;
 
-    /**
-     * {@code true} if the last element in {@link #arguments} shall be set to 
the erroneous value.
-     */
-    private final boolean needsValue;
-
     /**
      * The arguments to be used with the error message to format.
      * The current implementation relies on the following invariants:
@@ -71,7 +67,7 @@ final class Verifier {
      * <ul>
      *   <li>The first element in this array will be the parameter name. 
Before the name is known,
      *       this element is either {@code null} or the index to append to the 
name.</li>
-     *   <li>The last element shall be set to the erroneous value if {@link 
#needsValue} is {@code true}.</li>
+     *   <li>The last element may be set to the erroneous value.</li>
      * </ul>
      */
     private final Object[] arguments;
@@ -79,11 +75,10 @@ final class Verifier {
     /**
      * Stores information about an error.
      */
-    private Verifier(final boolean internal, final short errorKey, final 
boolean needsValue, final Object... arguments) {
-        this.errorKey   = errorKey;
-        this.internal   = internal;
-        this.needsValue = needsValue;
-        this.arguments  = arguments;
+    private Verifier(final boolean internal, final short errorKey, final 
Object... arguments) {
+        this.errorKey  = errorKey;
+        this.internal  = internal;
+        this.arguments = arguments;
     }
 
     /**
@@ -257,7 +252,7 @@ final class Verifier {
                     final Object[] arguments;
                     final Object minValue = valueDomain.getMinValue();
                     if ((minValue instanceof Number) && ((Number) 
minValue).doubleValue() == 0 && !valueDomain.isMinIncluded()
-                        && (value instanceof Number) && ((Number) 
value).doubleValue() < 0)
+                        && (value instanceof Number) && ((Number) 
value).doubleValue() <= 0)
                     {
                         errorKey     = Errors.Keys.ValueNotGreaterThanZero_2;
                         arguments    = new Object[2];
@@ -266,10 +261,10 @@ final class Verifier {
                         arguments    = new Object[4];
                         arguments[1] = minValue;
                         arguments[2] = valueDomain.getMaxValue();
-                    }
+                   }
                     if (isArray) arguments[0] = i;
                     arguments[arguments.length - 1] = value;
-                    return new Verifier(false, errorKey, true, arguments);
+                    return new Verifier(false, errorKey, arguments);
                 }
             }
         }
@@ -288,27 +283,30 @@ final class Verifier {
      * @param convertedValue  the value <em>converted to the units specified 
by the descriptor</em>.
      *        This is not necessarily the user-provided value.
      */
-    @SuppressWarnings("unchecked")
-    private static <T> Verifier ensureValidValue(final Class<T> valueClass, 
final Set<T> validValues,
-            final Comparable<T> minimum, final Comparable<T> maximum, final 
Object convertedValue)
+    @SuppressWarnings({"unchecked", "element-type-mismatch"})
+    private static <T> Verifier ensureValidValue(final Class<T> valueClass,
+                                                 final Set<T> validValues,
+                                                 final Comparable<T> minimum,
+                                                 final Comparable<T> maximum,
+                                                 final Object convertedValue)
     {
         if (!valueClass.isInstance(convertedValue)) {
             return new Verifier(true, 
Resources.Keys.IllegalParameterValueClass_3,
-                    false, null, valueClass, convertedValue.getClass());
+                    null, valueClass, convertedValue.getClass());
         }
         if (validValues != null && !validValues.contains(convertedValue)) {
-            return new Verifier(true, Resources.Keys.IllegalParameterValue_2, 
true, null, convertedValue);
+            return new Verifier(true, Resources.Keys.IllegalParameterValue_2, 
null, convertedValue);
         }
         if ((minimum != null && minimum.compareTo((T) convertedValue) > 0) ||
             (maximum != null && maximum.compareTo((T) convertedValue) < 0))
         {
-            return new Verifier(false, Errors.Keys.ValueOutOfRange_4, true, 
null, minimum, maximum, convertedValue);
+            return new Verifier(false, Errors.Keys.ValueOutOfRange_4, null, 
minimum, maximum, convertedValue);
         }
         return null;
     }
 
     /**
-     * Converts the information about an "value out of range" error. The range 
in the error message will be formatted
+     * Converts the information about a "value out of range" error. The range 
in the error message will be formatted
      * in the unit given by the user, which is not necessarily the same as the 
unit of the parameter descriptor.
      *
      * @param converter  the conversion from user unit to descriptor unit, or 
{@code null} if none.
@@ -317,12 +315,12 @@ final class Verifier {
     private void convertRange(UnitConverter converter) {
         if (converter != null && !internal && errorKey == 
Errors.Keys.ValueOutOfRange_4) {
             converter = converter.inverse();
-            Object minimumValue = arguments[1];
-            Object maximumValue = arguments[2];
-            minimumValue = (minimumValue != null) ? 
converter.convert(((Number) minimumValue).doubleValue()) : "−∞";
-            maximumValue = (maximumValue != null) ? 
converter.convert(((Number) maximumValue).doubleValue()) :  "∞";
-            arguments[1] = minimumValue;
-            arguments[2] = maximumValue;
+            for (int i=1; i<=2; i++) {
+                Object value = arguments[i];
+                if (value instanceof Number) {
+                    arguments[i] = converter.convert(((Number) 
value).doubleValue());
+                }
+            }
         }
     }
 
@@ -356,10 +354,31 @@ final class Verifier {
             value = Array.get(value, (Integer) index);
         }
         arguments[0] = name;
-        if (needsValue) {
-            arguments[arguments.length - 1] = value;
+        final IndexedResourceBundle resources;
+        if (internal) {
+            resources = Resources.forProperties(properties);
+            switch (errorKey) {
+                case Resources.Keys.IllegalParameterValue_2: {
+                    arguments[1] = value;
+                    break;
+                }
+            }
+        } else {
+            resources =  Errors.forProperties(properties);
+            switch (errorKey) {
+                case Errors.Keys.ValueOutOfRange_4: {
+                    if (arguments[1] == null) arguments[1] = "−∞";
+                    if (arguments[2] == null) arguments[2] =  "∞";
+                    arguments[3] = value;
+                    break;
+                }
+                case Errors.Keys.ValueNotGreaterThanZero_2: {
+                    arguments[1] = value;
+                    break;
+                }
+            }
         }
-        return (internal ? Resources.forProperties(properties) : 
Errors.forProperties(properties)).getString(errorKey, arguments);
+        return resources.getString(errorKey, arguments);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Wraparound.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Wraparound.java
index 2dfe8da087..7322091b9e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Wraparound.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Wraparound.java
@@ -76,7 +76,7 @@ public final class Wraparound extends AbstractProvider {
     public static final ParameterDescriptor<Integer> WRAPAROUND_DIMENSION;
 
     /**
-     * The operation parameter descriptor for for the period.
+     * The operation parameter descriptor for the period.
      *
      * <!-- Generated by ParameterNameTableGenerator -->
      * <table class="sis">
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
index 02e88a5827..beb7e1aea8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MolodenskyTransform.java
@@ -346,7 +346,7 @@ public class MolodenskyTransform extends 
DatumShiftTransform {
          * Prepare two affine transforms to be executed before and after the 
MolodenskyTransform:
          *
          *   - A "normalization" transform for converting degrees to radians,
-         *   - A "denormalization" transform for for converting radians to 
degrees.
+         *   - A "denormalization" transform for converting radians to degrees.
          */
         context.normalizeGeographicInputs(0);
         context.denormalizeGeographicOutputs(0);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
index 23d31dc2cf..cc82827fdb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/WraparoundTransform.java
@@ -68,7 +68,7 @@ import org.apache.sis.parameter.Parameters;
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.5
  *
- * @see org.apache.sis.geometry.Envelopes#wraparound(MathTransform, Envelope)
+ * @see 
org.apache.sis.geometry.Envelopes#transformWithWraparound(MathTransform, 
Envelope)
  *
  * @since 1.1
  */
@@ -108,7 +108,7 @@ public class WraparoundTransform extends 
AbstractMathTransform implements Serial
      * then {@code sourceMedian} should be 180° (the value at the center of [0 
… 360]° range).
      * The value may be {@link Double#NaN} if unknown.
      *
-     * <p>This field is used for inverse transforms only; it has no effect on 
the forward transforms.
+     * <p>This field is used for inverse transforms only. It has no effect on 
the forward transforms.
      * If not NaN, this value is used for building the transform returned by 
{@link #inverse()}.</p>
      *
      * <h4>Design note</h4>
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/geometry/EnvelopesTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/geometry/EnvelopesTest.java
index 23624aec8d..91cff34382 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/geometry/EnvelopesTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/geometry/EnvelopesTest.java
@@ -31,6 +31,7 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransform2D;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.measure.Range;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.crs.DefaultCompoundCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
@@ -309,7 +310,7 @@ public final class EnvelopesTest extends 
TransformTestCase<GeneralEnvelope> {
     }
 
     /**
-     * Tests {@link Envelopes#wraparound(MathTransform, Envelope)}.
+     * Tests {@link Envelopes#transformWithWraparound(MathTransform, 
Envelope)}.
      *
      * @throws TransformException if a coordinate transformation failed.
      */
@@ -320,10 +321,11 @@ public final class EnvelopesTest extends 
TransformTestCase<GeneralEnvelope> {
         envelope.setRange(1, 5, 9);
         final MathTransform tr = WraparoundTransform.create(2, 0, 360, -180, 
0);
         assertTrue(tr instanceof WraparoundTransform);
-        final GeneralEnvelope[] results = Envelopes.wraparound(tr, envelope);
-        assertEquals(2, results.length);
-        assertEnvelopeEquals(new GeneralEnvelope(new double[] {-200, 5}, new 
double[] {-100, 9}), results[0]);
-        assertEnvelopeEquals(new GeneralEnvelope(new double[] { 160, 5}, new 
double[] { 260, 9}), results[1]);
+        final Map<Parameters, GeneralEnvelope> results = 
Envelopes.transformWithWraparound(tr, envelope);
+        assertEquals(2, results.size());
+        final GeneralEnvelope[] envelopes = 
results.values().toArray(GeneralEnvelope[]::new);
+        assertEnvelopeEquals(new GeneralEnvelope(new double[] {-200, 5}, new 
double[] {-100, 9}), envelopes[0]);
+        assertEnvelopeEquals(new GeneralEnvelope(new double[] { 160, 5}, new 
double[] { 260, 9}), envelopes[1]);
     }
 
     /**


Reply via email to