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 d3eb5398af Take in account the fact that the "sign reversal" boolean 
flag depends on the operation which uses the operation method. Units of 
measurement may also be incompatible between two operations using the same 
operation method. Therefore, the `EPSGDataAccess` factory needs to work on a 
more case-by-case basis.
d3eb5398af is described below

commit d3eb5398afc80e0bfd8dabadf12dd233bfb6a8a8
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Sep 26 00:32:46 2025 +0200

    Take in account the fact that the "sign reversal" boolean flag depends on 
the operation which uses the operation method.
    Units of measurement may also be incompatible between two operations using 
the same operation method.
    Therefore, the `EPSGDataAccess` factory needs to work on a more 
case-by-case basis.
---
 .../referencing/factory/sql/EPSGDataAccess.java    | 660 +++++++++++++++------
 .../referencing/internal/SignReversalComment.java  |  10 +
 2 files changed, 479 insertions(+), 191 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index eaf0782262..8d890cb7e9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.ArrayList;
@@ -29,6 +30,7 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.StringJoiner;
 import java.util.stream.IntStream;
 import java.util.function.ToIntFunction;
 import java.util.logging.Level;
@@ -55,14 +57,13 @@ import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.util.Factory;
 import org.opengis.util.FactoryException;
-import org.opengis.util.NoSuchIdentifierException;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.OnLineFunction;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
-import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.parameter.GeneralParameterDescriptor;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.cs.*;
@@ -73,6 +74,7 @@ import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.DefaultObjectDomain;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
+import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.datum.DefaultDatumEnsemble;
@@ -213,6 +215,48 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      */
     private static final String UNKNOWN_SCOPE = "?";
 
+    /**
+     * An option added to the code of a parameter descriptor for specifying 
the type of parameter values.
+     * This is an undocumented extension specific to Apache <abbr>SIS</abbr>
+     *
+     * @see #getParameterType(int, Map)
+     * @see #separateOptions(String, Map)
+     * @see #createParameterDescriptor(String)
+     */
+    private static final String PARAMETER_TYPE_OPTION = "type";
+
+    /**
+     * Possible value for {@link #PARAMETER_TYPE_OPTION}.
+     */
+    private static final String URI_TYPE = "URI";
+
+    /**
+     * An option added to the code of a parameter descriptor for specifying 
the {@code uom_code} integer value.
+     * The same operation method may have different units of measurement and 
"sign reversal" flag depending on
+     * the operation which uses it, at least in the way that the 
<abbr>EPSG</abbr> database is structured.
+     * This is an undocumented extension specific to Apache <abbr>SIS</abbr>
+     *
+     * <h4>Example</h4>
+     * The EPSG:8617 (<cite>Coordinate 1 of evaluation point</cite>) parameter 
may be used in the
+     * <abbr>EPSG</abbr> database with either meters or degrees units, 
depending on which operation
+     * uses that parameter.
+     *
+     * @see #getParameterUnit(int, Map)
+     * @see #separateOptions(String, Map)
+     * @see #createParameterDescriptor(String)
+     */
+    private static final String UOM_CODE_OPTION = "uom_code";
+
+    /**
+     * An option added to the code of a parameter descriptor for specifying 
the {@code sign_reversal} Boolean value.
+     * This is an undocumented extension specific to Apache <abbr>SIS</abbr>
+     *
+     * @see #getSignReversal(int, Map)
+     * @see #separateOptions(String, Map)
+     * @see #createParameterDescriptor(String)
+     */
+    private static final String SIGN_REVERSAL_OPTION = "sign_reversal";
+
     /**
      * The namespace of EPSG names and codes. This namespace is needed by all 
{@code createFoo(String)} methods.
      * The {@code EPSGDataAccess} constructor relies on the {@link 
EPSGFactory#nameFactory} caching mechanism
@@ -264,7 +308,8 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * However, this duplication should not happen often. For example, each 
conventional <abbr>RS</abbr> should appear
      * in only one datum ensemble created by {@link 
#createDatumEnsemble(Integer, Map)}.
      *
-     * <p>Keys are {@link Long} except the keys for naming systems which are 
{@link String}.</p>
+     * <p>Keys are {@link Long} except the keys for naming systems which are 
{@link String}.
+     * The {@code Long} values are computed by {@link #cacheKey(int, int)}.</p>
      *
      * @see #getAxisName(Integer)
      * @see #getRealizationMethod(Integer)
@@ -314,7 +359,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
 
     /**
      * Identification of a query under execution. The query is identified by a 
table name and the <abbr>EPSG</abbr>
-     * code(s) of the object to search. It is legal to have have two queries 
in progress on the same table, provided
+     * code(s) of the object to search. It is legal to have two queries in 
progress on the same table, provided
      * that the <abbr>EPSG</abbr> codes are different. For example, if a 
projected <abbr>CRS</abbr> is read from the
      * "Coordinate Reference System" table, that query will require another 
query on the same table but for the base
      * geographic <abbr>CRS</abbr>. Therefore, a limited form of recursive 
queries need to be accepted, but we want
@@ -680,6 +725,56 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
         return Optional.empty();
     }
 
+    /**
+     * Formats a code with options. The returned string can be parsed by 
{@link #separateOptions(String, Map)}.
+     * This is used for passing options when creating an object from an 
authority code through public <abbr>API</abbr>.
+     * Formatting options with the authority code allows to differentiate 
variants in the {@linkplain #owner} cache.
+     * This is specific to Apache <abbr>SIS</abbr>.
+     *
+     * @param  code     the raw authority code, without options.
+     * @param  options  the options to format in order. Should be a map with 
deterministic entry order.
+     * @return the given code with the given options appended.
+     */
+    private static String codeWithOptions(final String code, final Map<String, 
String> options) {
+        if (options.isEmpty()) {
+            return code;
+        }
+        final var s = new StringBuilder(code);
+        char separator = '?';
+        for (final Map.Entry<String, String> entry : options.entrySet()) {
+            
s.append(separator).append(entry.getKey()).append('=').append(entry.getValue());
+            separator = '&';
+        }
+        return s.toString();
+    }
+
+    /**
+     * If the given code has any options, stores them in the given map and 
returns the code alone.
+     * The keys are strings defined by the {@code *_OPTION} constants in this 
class.
+     * Example: {@code 8623?uom_code=9001&sign_reversal=false} (a parameter 
descriptor).
+     *
+     * @param  code     the code potentially followed by options.
+     * @param  options  the map where to store the options.
+     * @return the code without options.
+     *
+     * @see #UOM_CODE_OPTION
+     * @see #SIGN_REVERSAL_OPTION
+     * @see #createParameterDescriptor(String)
+     */
+    private static String separateOptions(final String code, final Map<String, 
String> options) {
+        final int s = code.indexOf('?');
+        if (s < 0) {
+            return code;
+        }
+        for (String option : (String[]) 
CharSequences.split(code.substring(s+1), '&')) {
+            final int e = option.indexOf('=');
+            if (s >= 0) {
+                options.put(option.substring(0, 
e).trim().toLowerCase(Locale.US), option.substring(e+1).trim());
+            }
+        }
+        return code.substring(0, s);
+    }
+
     /**
      * Returns {@code true} if the specified code may be a primary key in some 
tables.
      * This method does not need to check any entry in the database.
@@ -1839,7 +1934,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
             currentSingletonQuery = previousSingletonQuery;
         }
         if (returnValue == null) {
-             throw noSuchAuthorityCode(CoordinateReferenceSystem.class, code);
+            throw noSuchAuthorityCode(CoordinateReferenceSystem.class, code);
         }
         return returnValue;
     }
@@ -2080,7 +2175,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
          * @return the component created from the given code.
          * @throws FactoryException if an error occurred while creating the 
component.
          */
-        C create(GeodeticAuthorityFactory factory, String code) throws 
FactoryException;
+        C create(GeodeticAuthorityFactory factory, String code) throws 
SQLException, FactoryException;
     }
 
     /**
@@ -2183,10 +2278,10 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 final double  semiMajorAxis     = getDouble   (code, result, 
3);
                 final double  inverseFlattening = getOptionalDouble (result, 
4);
                 final double  semiMinorAxis     = getOptionalDouble (result, 
5);
-                final String  unitCode          = getString   (code, result, 
6);
+                final String  uom_code          = getString   (code, result, 
6);
                 final String  remarks           = getOptionalString (result, 
7);
                 final boolean deprecated        = getOptionalBoolean(result, 
8);
-                final Unit<Length> unit         = 
owner.createUnit(unitCode).asType(Length.class);
+                final Unit<Length> unit         = 
owner.createUnit(uom_code).asType(Length.class);
                 final boolean useSemiMinor      = 
Double.isNaN(inverseFlattening);
                 if (useSemiMinor && Double.isNaN(semiMinorAxis)) {
                     // Both are null, which is not allowed.
@@ -2225,7 +2320,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
             currentSingletonQuery = previousSingletonQuery;
         }
         if (returnValue == null) {
-             throw noSuchAuthorityCode(Ellipsoid.class, code);
+            throw noSuchAuthorityCode(Ellipsoid.class, code);
         }
         return returnValue;
     }
@@ -2276,10 +2371,10 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 final Integer epsg       = getInteger  (code, result, 1);
                 final String  name       = getString   (code, result, 2);
                 final double  longitude  = getDouble   (code, result, 3);
-                final String  unitCode   = getString   (code, result, 4);
+                final String  uom_code   = getString   (code, result, 4);
                 final String  remarks    = getOptionalString (result, 5);
                 final boolean deprecated = getOptionalBoolean(result, 6);
-                final Unit<Angle> unit = 
owner.createUnit(unitCode).asType(Angle.class);
+                final Unit<Angle> unit = 
owner.createUnit(uom_code).asType(Angle.class);
                 /*
                  * Map of properties should be populated only after we 
extracted all
                  * information needed from the `ResultSet`, because it may be 
closed.
@@ -2643,7 +2738,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 final Integer nameCode     = getInteger(code, result, 2);
                 final String  orientation  = getString (code, result, 3);
                 final String  abbreviation = getString (code, result, 4);
-                final String  unit         = getString (code, result, 5);
+                final String  uom_code     = getString (code, result, 5);
                 final AxisDirection direction;
                 try {
                     direction = 
CoordinateSystems.parseAxisDirection(orientation);
@@ -2657,7 +2752,7 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                  */
                 final CoordinateSystemAxis axis = 
owner.csFactory.createCoordinateSystemAxis(
                         createProperties(TableInfo.AXIS, epsg, an.name, 
an.description, null, null, an.remarks, false),
-                        abbreviation, direction, owner.createUnit(unit));
+                        abbreviation, direction, owner.createUnit(uom_code));
                 returnValue = ensureSingleton(axis, returnValue, code);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
             }
@@ -2820,6 +2915,119 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
         return returnValue;
     }
 
+    /**
+     * Determines the type of values in a parameter. If the parameter has at 
least one non-null value in the
+     * "Parameter File Name" column, then the value type is assumed to be 
<abbr>URI</abbr> stored as string.
+     * Otherwise, the type is assumed floating point number.
+     *
+     * @param  parameter  the <abbr>EPSG</abbr> code of the parameter 
descriptor.
+     * @param  options    the options where to store the {@value 
#PARAMETER_TYPE_OPTION} value.
+     */
+    private void getParameterType(final int parameter, final Map<String, 
String> options) throws SQLException {
+        try (ResultSet result = executeQueryForCodes(
+                "Parameter Type",
+                "SELECT PARAM_VALUE_FILE_REF"
+                        + " FROM \"Coordinate_Operation Parameter Value\""
+                        + " WHERE PARAM_VALUE_FILE_REF IS NOT NULL"
+                        + " AND (PARAMETER_CODE = ?)", parameter))
+        {
+            while (result.next()) {
+                String element = getOptionalString(result, 1);
+                if (element != null && !element.isBlank()) {
+                    options.put(PARAMETER_TYPE_OPTION, URI_TYPE);
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * Determines the most frequently used units of measurement of a parameter.
+     * We can have many different units for the same parameter, but usually 
all units have the same dimension.
+     * For example, we may have meters, kilometers, and feet. In such case, 
the set of units will have only
+     * one element and that element will be the most frequently used unit. 
However, some parameters accept
+     * units of different dimensions. For example, the "Coordinate 1 of 
evaluation point" (EPSG:8617) parameter
+     * may be in meters or in degrees. In such case, the set of units will 
have two elements.
+     *
+     * @param  parameter  the <abbr>EPSG</abbr> code of the parameter 
descriptor.
+     * @param  options    the options where to store the {@value 
#UOM_CODE_OPTION} value.
+     * @param  dimension  if non-null, consider only the units compatible with 
{@code dimension}.
+     */
+    private void getParameterUnit(final int parameter, final Map<String, 
String> options, final Unit<?> dimension)
+            throws SQLException, FactoryException
+    {
+        final var codes = new StringJoiner(",");
+        final var units = new ArrayList<Unit<?>>();
+        try (ResultSet result = executeQueryForCodes(
+                "Parameter Unit",
+                "SELECT UOM_CODE"
+                        + " FROM \"Coordinate_Operation Parameter Value\""
+                        + " WHERE (PARAMETER_CODE = ?)"
+                        + " GROUP BY UOM_CODE"
+                        + " ORDER BY COUNT(UOM_CODE) DESC", parameter))
+        {
+next:       while (result.next()) {
+                final String uom_code = getOptionalString(result, 1);
+                if (uom_code != null) {
+                    final Unit<?> candidate = owner.createUnit(uom_code);
+                    if (dimension != null && 
!candidate.isCompatible(dimension)) {
+                        continue;
+                    }
+                    for (final Unit<?> e : units) {
+                        if (candidate.isCompatible(e)) {
+                            continue next;
+                        }
+                    }
+                    units.add(candidate);
+                    codes.add(uom_code);
+                }
+            }
+        }
+        if (codes.length() != 0) {
+            options.put(UOM_CODE_OPTION, codes.toString());
+        }
+    }
+
+    /**
+     * Determines if the inverse operation can be performed by reversing the 
parameter sign.
+     * The <abbr>EPSG</abbr> dataset uses "Yes" or "No" value while 
<abbr>SIS</abbr> scripts
+     * use Boolean type. This method accepts the two forms. If a string is not 
recognized as
+     * a Boolean value, then this method throws a {@link SQLException} because 
a wrong value
+     * would let {@code EPSGDataAccess} finishes its work without apparent 
problem but would
+     * cause failures later when Apache <abbr>SIS</abbr> tries to infer an 
inverse operation.
+     * An exception thrown at a later time is much more difficult to relate to 
the root cause
+     * than if we throw the exception here.
+     *
+     * @param  parameter  the <abbr>EPSG</abbr> code of the parameter 
descriptor.
+     * @param  options    the options where to store the {@value 
#SIGN_REVERSAL_OPTION} value.
+     */
+    private void getSignReversal(final int parameter, final Map<String, 
String> options) throws SQLException {
+        try (ResultSet result = executeQueryForCodes(
+                "Sign Reversal",
+                "SELECT DISTINCT PARAM_SIGN_REVERSAL"
+                        + " FROM \"Coordinate_Operation Parameter Usage\""
+                        + " WHERE (PARAMETER_CODE = ?)", parameter))
+        {
+            Boolean reversibility = null;
+            while (result.next()) {
+                Boolean value;
+                if (translator.useBoolean()) {
+                    value = result.getBoolean(1);
+                    if (result.wasNull()) return;
+                } else {
+                    // May throw SQLException - see above comment.
+                    value = SQLUtilities.parseBoolean(result.getString(1));
+                    if (value == null) return;
+                }
+                if (reversibility == null) reversibility = value;
+                else if (!reversibility.equals(value)) return;
+            }
+            if (reversibility != null) {
+                options.put(SIGN_REVERSAL_OPTION, reversibility.toString());
+            }
+        }
+    }
+
     /**
      * Creates a definition of a single parameter used by an operation method.
      *
@@ -2848,6 +3056,25 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
             throws NoSuchAuthorityCodeException, FactoryException
     {
         ArgumentChecks.ensureNonNull("code", code);
+        final var options = new HashMap<String, String>(4);
+        final String base = separateOptions(code, options);
+        /*
+         * If the code has options (e.g., 
"8623?uom_code=9001&sign_reversal=false"),
+         * get the descriptor with default options, then amends that 
descriptor with
+         * the given options. We take this approach for leveraging the `owner` 
cache.
+         */
+        if (!options.isEmpty()) try {
+            final ParameterDescriptor<?> generic = 
owner.createParameterDescriptor(base);
+            final var metadata = new HashMap<String, 
Object>(IdentifiedObjects.getProperties(generic));
+            final var returnValue = 
createParameterDescriptor(Integer.valueOf(base), metadata, options);
+            return generic.equals(returnValue) ? generic : returnValue;     // 
Share the existing instance.
+        } catch (SQLException exception) {
+            throw databaseFailure(OperationMethod.class, code, exception);
+        }
+        /*
+         * Case of parameters without options. This case is indirectly 
executed even when
+         * options were present, in order to get the base parameter to be used 
as template.
+         */
         ParameterDescriptor<?> returnValue = null;
         final QueryID previousSingletonQuery = currentSingletonQuery;
         try (ResultSet result = executeSingletonQuery(
@@ -2864,117 +3091,13 @@ search: try (ResultSet result = 
executeMetadataQuery("Deprecation",
                 final String  name        = getString   (code, result, 2);
                 final String  description = getOptionalString (result, 3);
                 final boolean deprecated  = getOptionalBoolean(result, 4);
-                /*
-                 * If the parameter is an integer code, the type is integer 
and there is no unit.
-                 */
-                Class<?> type;
-                final Set<Unit<?>> units;
-                if (epsg != null && EPSG_CODE_PARAMETERS.contains(epsg)) {
-                    type  = Integer.class;
-                    units = Set.of();
-                } else {
-                    /*
-                     * If the parameter appears to have at least one non-null 
value in the "Parameter File Name" column,
-                     * then the type is assumed to be URI as a string. 
Otherwise, the type is a floating point number.
-                     */
-                    type = Double.class;
-                    try (ResultSet r = executeQueryForCodes(
-                            "Parameter Type",
-                            "SELECT PARAM_VALUE_FILE_REF"
-                                    + " FROM \"Coordinate_Operation Parameter 
Value\""
-                                    + " WHERE PARAM_VALUE_FILE_REF IS NOT NULL"
-                                    + " AND (PARAMETER_CODE = ?)", epsg))
-                    {
-                        while (r.next()) {
-                            String element = getOptionalString(r, 1);
-                            if (element != null && !element.isEmpty()) {
-                                type = String.class;
-                                break;
-                            }
-                        }
-                    }
-                    /*
-                     * Search for units.   We typically have many different 
units but all of the same dimension
-                     * (for example metres, kilometres, feet, etc.). In such 
case, the units Set will have only
-                     * one element and that element will be the most 
frequently used unit.  But some parameters
-                     * accept units of different dimensions. For example, the 
"Coordinate 1 of evaluation point"
-                     * (EPSG:8617) parameter value may be in metres or in 
degrees.   In such case the units Set
-                     * will have two elements.
-                     */
-                    units = new LinkedHashSet<>();
-                    try (ResultSet r = executeQueryForCodes(
-                            "Parameter Unit",
-                            "SELECT UOM_CODE"
-                                    + " FROM \"Coordinate_Operation Parameter 
Value\""
-                                    + " WHERE (PARAMETER_CODE = ?)"
-                                    + " GROUP BY UOM_CODE"
-                                    + " ORDER BY COUNT(UOM_CODE) DESC", epsg))
-                    {
-next:                   while (r.next()) {
-                            final String c = getOptionalString(r, 1);
-                            if (c != null) {
-                                final Unit<?> candidate = owner.createUnit(c);
-                                for (final Unit<?> e : units) {
-                                    if (candidate.isCompatible(e)) {
-                                        continue next;
-                                    }
-                                }
-                                units.add(candidate);
-                            }
-                        }
-                    }
-                }
-                /*
-                 * Determines if the inverse operation can be performed by 
reversing the parameter sign.
-                 * The EPSG dataset uses "Yes" or "No" value, but SIS scripts 
use boolean type. We have
-                 * to accept both. Note that if we do not recognize the string 
as a boolean value, then
-                 * we need a SQLException, not a null value.  If the value is 
wrongly null, this method
-                 * will succeed anyway and EPSGDataAccess will finish its work 
without apparent problem,
-                 * but Apache SIS will fail later when it will try to compute 
the inverse operation, for
-                 * example in a call to CRS.findOperation(…). The exception 
thrown at such later time is
-                 * much more difficult to relate to the root cause than if we 
throw the exception here.
-                 */
-                InternationalString isReversible = null;
-                try (ResultSet r = executeQueryForCodes(
-                        "Parameter Sign",
-                        "SELECT DISTINCT PARAM_SIGN_REVERSAL"
-                                + " FROM \"Coordinate_Operation Parameter 
Usage\""
-                                + " WHERE (PARAMETER_CODE = ?)", epsg))
-                {
-                    if (r.next()) {
-                        Boolean b;
-                        if (translator.useBoolean()) {
-                            b = r.getBoolean(1);
-                            if (r.wasNull()) b = null;
-                        } else {
-                            b = SQLUtilities.parseBoolean(r.getString(1));  // 
May throw SQLException - see above comment.
-                        }
-                        if (b != null) {
-                            isReversible = b ? SignReversalComment.OPPOSITE : 
SignReversalComment.SAME;
-                        }
-                    }
-                }
-                /*
-                 * Now creates the parameter descriptor.
-                 */
-                final NumberRange<?> valueDomain;
-                switch (units.size()) {
-                    case 0:  valueDomain = null; break;
-                    default: valueDomain = new EPSGParameterDomain(units); 
break;
-                    case 1:  valueDomain = 
MeasurementRange.create(Double.NEGATIVE_INFINITY, false,
-                                                                   
Double.POSITIVE_INFINITY, false,
-                                                                   
CollectionsExt.first(units)); break;
-                }
-                /*
-                 * Map of properties should be populated only after we 
extracted all
-                 * information needed from the `ResultSet`, because it may be 
closed.
-                 */
+                getParameterUnit(epsg, options, null);
+                getParameterType(epsg, options);
+                getSignReversal (epsg, options);
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
-                        TableInfo.PARAMETER, epsg, name, null, null, null, 
isReversible, deprecated);
-                properties.put(Identifier.DESCRIPTION_KEY, description);
-                final var descriptor = new 
DefaultParameterDescriptor<>(properties, 1, 1, type, valueDomain, null, null);
-                returnValue = ensureSingleton(descriptor, returnValue, code);
+                        TableInfo.PARAMETER, epsg, name, description, null, 
null, null, deprecated);
+                returnValue = createParameterDescriptor(epsg, properties, 
options);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
             }
         } catch (SQLException exception) {
@@ -2983,92 +3106,237 @@ next:                   while (r.next()) {
             currentSingletonQuery = previousSingletonQuery;
         }
         if (returnValue == null) {
-             throw noSuchAuthorityCode(OperationMethod.class, code);
+            throw noSuchAuthorityCode(OperationMethod.class, code);
         }
         return returnValue;
     }
 
     /**
-     * Sets the values of all parameters in the given group.
+     * Creates a parameter descriptor from the given properties.
+     * The given maps shall be modifiable, as this method will modify them.
+     *
+     * @param  code      value allocated by EPSG.
+     * @param  metadata  the properties fetched from the database or from 
another descriptor.
+     * @param  options   the value type, unit of measurement and sign reversal.
+     * @return the parameter descriptor for the given code, properties and 
options.
+     */
+    private ParameterDescriptor<?> createParameterDescriptor(final Integer 
code,
+            final Map<String, Object> metadata, final Map<String, String> 
options)
+            throws SQLException, FactoryException
+    {
+        final Class<?> type;
+        final Set<Unit<?>> units;
+        if (EPSG_CODE_PARAMETERS.contains(code)) {
+            // If the parameter is an EPSG code, the type is integer and there 
is no unit.
+            type  = Integer.class;
+            units = Set.of();
+        } else {
+            if 
(URI_TYPE.equalsIgnoreCase(options.remove(PARAMETER_TYPE_OPTION))) {
+                type = String.class;
+            } else {
+                type = Double.class;
+            }
+            units = new LinkedHashSet<>();
+            for (String uom_code : (String[]) 
CharSequences.split(options.remove(UOM_CODE_OPTION), ',')) {
+                units.add(owner.createUnit(uom_code));
+            }
+        }
+        /*
+         * Determine if the inverse operation can be performed by reversing 
the parameter sign.
+         * The `SignReversalComment` is used as a sentinel value in other 
Apache SIS packages,
+         * so this information needs to be accurate. For that reason, if we 
fail to parse the
+         * Boolean value, it is better to throw an exception now rather than 
later.
+         */
+        metadata.put(IdentifiedObject.REMARKS_KEY, SignReversalComment.of(
+                
SQLUtilities.parseBoolean(options.remove(SIGN_REVERSAL_OPTION))));
+        /*
+         * Now creates the parameter descriptor.
+         */
+        final NumberRange<?> valueDomain;
+        switch (units.size()) {
+            case 0:  valueDomain = null; break;
+            default: valueDomain = new EPSGParameterDomain(units); break;
+            case 1:  valueDomain = 
MeasurementRange.create(Double.NEGATIVE_INFINITY, false,
+                                                           
Double.POSITIVE_INFINITY, false,
+                                                           
CollectionsExt.first(units)); break;
+        }
+        return new DefaultParameterDescriptor<>(metadata, 1, 1, type, 
valueDomain, null, null);
+    }
+
+    /**
+     * Temporary storage for parameter values before they are set in the 
parameter group.
      *
-     * @param  method      the EPSG code for the operation method.
-     * @param  operation   the EPSG code for the operation (conversion or 
transformation).
-     * @param  parameters  the parameter values to fill.
+     * <h2>Purpose</h2>
+     * The {@link ParameterDescriptor} and {@link ParameterValue} instances 
are created together,
+     * because the parameter descriptors for the same operation method may 
vary slightly depending
+     * on which coordinate operation use that method. But we cannot set the 
{@link ParameterValue}s
+     * before all descriptors have been created and put in a {@link 
ParameterValueGroup}.
+     * Hence, we temporarily store the values in this object until the group 
is created.
+     */
+    private static final class Parameter {
+        /**
+         * The descriptor of the parameter.
+         */
+        final ParameterDescriptor<?> descriptor;
+
+        /**
+         * Value of the {@code PARAM_VALUE_FILE_REF} column, or {@code null} 
if none.
+         * It may be a file in the {@code "$SIS_DATA/DatumChanges"} directory.
+         * Should be relative and <em>not</em> encoded for valid 
<abbr>URI</abbr> syntax.
+         * The encoding will be applied by invoking the {@link URI} 
multi-argument constructor.
+         */
+        final String reference;
+
+        /**
+         * Value of the {@code PARAMETER_VALUE} column.
+         * Ignored if {@code PARAM_VALUE_FILE_REF} is non-null.
+         */
+        final double value;
+
+        /**
+         * Value of the {@code UOM_CODE} column, or {@code null} if none.
+         */
+        final Unit<?> unit;
+
+        /**
+         * Creates a new value for the given descriptor.
+         * Callers shall set at least one of the non-final fields after 
construction.
+         */
+        Parameter(final ParameterDescriptor<?> descriptor, final String 
reference, final double value, final Unit<?> unit) {
+            this.descriptor = descriptor;
+            this.reference  = reference;
+            this.value      = value;
+            this.unit       = unit;
+        }
+
+        /**
+         * Returns an operation method with the same metadata than the given 
method,
+         * but with the descriptors of the parameters in the given list.
+         * Those parameter descriptors should be the same as the parameters of 
the given method.
+         * But sometime, the parameters differ in units of measurement or in 
sign reversal flag.
+         *
+         * @param  generic  a generic operation method for no operation in 
particular.
+         * @return the given operation method potentially fitted to a 
particular operation.
+         */
+        static OperationMethod recreateIfChanged(OperationMethod generic, 
final List<Parameter> values) {
+            final List<GeneralParameterDescriptor> existing = 
generic.getParameters().descriptors();
+            final var descriptors = new ParameterDescriptor<?>[values.size()];
+            boolean changed = false;
+            for (int i=0; i<descriptors.length; i++) {
+                ParameterDescriptor<?> descriptor = values.get(i).descriptor;
+                final GeneralParameterDescriptor other = existing.get(i);
+                if (descriptor.equals(other)) {
+                    descriptor = (ParameterDescriptor<?>) other;    // Share 
existing instances.
+                } else {
+                    changed = true;
+                }
+                descriptors[i] = descriptor;
+            }
+            if (changed) {
+                // Rebuild the operation with slightly different parameter 
descriptors.
+                generic = new DefaultOperationMethod(
+                        IdentifiedObjects.getProperties(generic),
+                        new DefaultParameterDescriptorGroup(
+                                
IdentifiedObjects.getProperties(generic.getParameters()),
+                                1, 1, descriptors));
+            }
+            return generic;
+        }
+
+        /**
+         * Sets the value in the given group of parameters.
+         *
+         * @param  target  where to set the value.
+         */
+        final void setValue(final ParameterValueGroup target) throws 
URISyntaxException {
+            final ParameterValue<?> param = target.parameter(name());
+            if (reference != null) {
+                param.setValue(new URI(null, reference, null));
+                return;
+            }
+            if (unit != null) {
+                if (!Units.UNITY.equals(unit) || param.getUnit() != null) {
+                    param.setValue(value, unit);
+                    return;
+                }
+            }
+            param.setValue(value);
+        }
+
+        /**
+         * Returns the parameter name.
+         */
+        final String name() {
+            return descriptor.getName().getCode();
+        }
+
+        /**
+         * Returns a string representation for debugging purposes.
+         */
+        @Override
+        public String toString() {
+            final var s = new StringBuilder(name()).append(" = ");
+            if (reference != null) {
+                s.append(reference);
+            } else {
+                s.append(value);
+                if (unit != null) {
+                    s.append(' ').append(unit);
+                }
+            }
+            return s.toString();
+        }
+    }
+
+    /**
+     * Creates the parameters descriptors and their values for all parameters 
of the given operation.
+     * The descriptors are created in same time than their values because some 
descriptor metadata,
+     * such as units of measurement and aliases, may differ for different 
operations.
+     *
+     * @param  operation  the EPSG code for the operation (conversion or 
transformation).
+     * @param  method     the EPSG code for the method used by the specified 
operation.
+     * @return the parameter values with their descriptors.
      * @throws SQLException if a SQL statement failed.
      */
-    private void fillParameterValues(final Integer method, final Integer 
operation, final ParameterValueGroup parameters)
+    private List<Parameter> createParameterValues(final Integer operation, 
final int method)
             throws FactoryException, SQLException
     {
+        final var parameters = new ArrayList<Parameter>();
         try (ResultSet result = executeQueryForCodes(
                 "Coordinate_Operation Parameter Value",
-                "SELECT"+ /* column 1 */ " CP.PARAMETER_NAME,"
+                "SELECT"+ /* column 1 */ " CV.PARAMETER_CODE,"
                         + /* column 2 */ " CV.PARAMETER_VALUE,"
                         + /* column 3 */ " CV.PARAM_VALUE_FILE_REF,"
-                        + /* column 4 */ " CV.UOM_CODE"
-                        + " FROM ("      + "\"Coordinate_Operation Parameter 
Value\"" + " AS CV"
-                        + " INNER JOIN " + "\"Coordinate_Operation 
Parameter\""       + " AS CP" + " ON (CV.PARAMETER_CODE = CP.PARAMETER_CODE))"
-                        + " INNER JOIN " + "\"Coordinate_Operation Parameter 
Usage\"" + " AS CU" + " ON (CP.PARAMETER_CODE = CU.PARAMETER_CODE)"
+                        + /* column 4 */ " CV.UOM_CODE,"
+                        + /* column 5 */ " CU.PARAM_SIGN_REVERSAL"
+                        + " FROM "       + "\"Coordinate_Operation Parameter 
Value\"" + " AS CV"
+                        + " INNER JOIN " + "\"Coordinate_Operation Parameter 
Usage\"" + " AS CU"
+                        +   " ON (CV.PARAMETER_CODE"  + " = CU.PARAMETER_CODE)"
                         +  " AND (CV.COORD_OP_METHOD_CODE = 
CU.COORD_OP_METHOD_CODE)"
                         + " WHERE CV.COORD_OP_METHOD_CODE = ?"
                         +   " AND CV.COORD_OP_CODE = ?"
                         + " ORDER BY CU.SORT_ORDER", method, operation))
         {
             while (result.next()) {
-                final String name = getString(operation, result, 1);
-                final ParameterValue<?> param;
-                try {
-                    param = parameters.parameter(name);
-                } catch (ParameterNotFoundException exception) {
-                    /*
-                     * Wrap the unchecked ParameterNotFoundException into the 
checked NoSuchIdentifierException,
-                     * which is a FactoryException subclass.  Note that in 
principle, NoSuchIdentifierException is for
-                     * MathTransforms rather than parameters. However, we are 
close in spirit here since we are setting
-                     * up MathTransform's parameters. Using 
NoSuchIdentifierException allows CoordinateOperationSet to
-                     * know that the failure is probably caused by a 
MathTransform not yet supported in Apache SIS
-                     * (or only partially supported) rather than some more 
serious failure in the database side.
-                     * Callers can use this information in order to determine 
if they should try the next coordinate
-                     * operation or propagate the exception.
-                     */
-                    throw (NoSuchIdentifierException) new 
NoSuchIdentifierException(error().getString(
-                            Errors.Keys.CanNotSetParameterValue_1, name), 
name).initCause(exception);
+                final Integer descriptor    = getInteger(operation, result, 1);
+                final double  value         = getOptionalDouble(result, 2);
+                final String  reference     = Double.isNaN(value) ? 
getString(operation, result, 3) : null;
+                final String  uom_code      = getOptionalString(result, 4);
+                final Unit<?> unit          = (uom_code != null) ? 
owner.createUnit(uom_code) : null;
+                final Boolean reversibility = getOptionalBoolean(result, 5);
+                final var     options       = new LinkedHashMap<String, 
String>(8);    // The cache needs stable order.
+                if (reference != null) {
+                    options.put(PARAMETER_TYPE_OPTION, URI_TYPE);
                 }
-                final double value = getOptionalDouble(result, 2);
-                Unit<?> unit = null;
-                String reference;
-                if (Double.isNaN(value)) {
-                    /*
-                     * If no numeric value was provided in the database, then 
the values should be in
-                     * an external file. It may be a file in the 
"$SIS_DATA/DatumChanges" directory.
-                     * The reference file should be relative and _not_ encoded 
for valid URI syntax.
-                     * The encoding will be applied by invoking an `URI` 
multi-argument constructor.
-                     * Note that we must use a multi-arguments constructor, 
not URI(String), because
-                     * the latter assumes an encoded string (which is not the 
case in EPSG database).
-                     */
-                    reference = getString(operation, result, 3);
-                } else {
-                    reference = null;
-                    final String unitCode = getOptionalString(result, 4);
-                    if (unitCode != null) {
-                        unit = owner.createUnit(unitCode);
-                        if (Units.UNITY.equals(unit) && param.getUnit() == 
null) {
-                            unit = null;
-                        }
-                    }
-                }
-                try {
-                    if (reference != null) {
-                        param.setValue(new URI(null, reference, null));     // 
See above comment.
-                    } else if (unit != null) {
-                        param.setValue(value, unit);
-                    } else {
-                        param.setValue(value);
-                    }
-                } catch (RuntimeException | URISyntaxException exception) {
-                    // Catch InvalidParameterValueException, 
ArithmeticException and others.
-                    throw new 
FactoryDataException(error().getString(Errors.Keys.CanNotSetParameterValue_1, 
name), exception);
+                getParameterUnit(descriptor, options, unit);
+                if (reversibility != null) {
+                    options.put(SIGN_REVERSAL_OPTION, 
reversibility.toString());
                 }
+                final String codeWithOptions = 
codeWithOptions(descriptor.toString(), options);
+                parameters.add(new 
Parameter(owner.createParameterDescriptor(codeWithOptions), reference, value, 
unit));
             }
         }
+        return parameters;
     }
 
     /**
@@ -3125,14 +3393,17 @@ next:                   while (r.next()) {
                 /*
                  * Map of properties should be populated only after we 
extracted all
                  * information needed from the `ResultSet`, because it may be 
closed.
+                 * Note: we do not store the formula at this time because the 
text is
+                 * very verbose and rarely used.
                  */
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final Map<String,Object> properties = createProperties(
                         TableInfo.METHOD, epsg, name, null, null, null, 
remarks, deprecated);
-                /*
-                 * Note: we do not store the formula at this time, because the 
text is very verbose and rarely used.
-                 */
+
+                final Object identifier = 
properties.remove(IdentifiedObject.IDENTIFIERS_KEY);
                 final var params = new 
DefaultParameterDescriptorGroup(properties, 1, 1, descriptors);
+                properties.putAll(IdentifiedObjects.getProperties(params));    
 // For sharing existing instances.
+                properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifier);
                 final var method = new DefaultOperationMethod(properties, 
params);
                 returnValue = ensureSingleton(method, returnValue, code);
                 if (result.isClosed()) break;   // See createProperties(…) for 
explanation.
@@ -3143,7 +3414,7 @@ next:                   while (r.next()) {
             currentSingletonQuery = previousSingletonQuery;
         }
         if (returnValue == null) {
-             throw noSuchAuthorityCode(OperationMethod.class, code);
+            throw noSuchAuthorityCode(OperationMethod.class, code);
         }
         return returnValue;
     }
@@ -3241,9 +3512,16 @@ next:                   while (r.next()) {
                 final ParameterValueGroup parameterValues;
                 final boolean isDeferred = 
Semaphores.query(Semaphores.METADATA_ONLY);
                 if (methodCode != null && !isDeferred) {
-                    operationMethod = 
owner.createOperationMethod(methodCode.toString());
+                    final OperationMethod generic = 
owner.createOperationMethod(methodCode.toString());
+                    final List<Parameter> values = createParameterValues(epsg, 
methodCode);
+                    operationMethod = Parameter.recreateIfChanged(generic, 
values);
                     parameterValues = 
operationMethod.getParameters().createValue();
-                    fillParameterValues(methodCode, epsg, parameterValues);
+                    for (final Parameter element : values) try {
+                        element.setValue(parameterValues);
+                    } catch (RuntimeException | URISyntaxException exception) {
+                        String message = 
error().getString(Errors.Keys.CanNotSetParameterValue_1, element.name());
+                        throw new FactoryDataException(message, exception);
+                    }
                 } else {
                     operationMethod = null;
                     parameterValues = null;
@@ -3339,7 +3617,7 @@ next:                   while (r.next()) {
             currentSingletonQuery = previousSingletonQuery;
         }
         if (returnValue == null) {
-             throw noSuchAuthorityCode(CoordinateOperation.class, code);
+            throw noSuchAuthorityCode(CoordinateOperation.class, code);
         }
         return returnValue;
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/SignReversalComment.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/SignReversalComment.java
index d69cbba856..3cc51e2fe3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/SignReversalComment.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/SignReversalComment.java
@@ -61,6 +61,16 @@ public final class SignReversalComment extends 
AbstractInternationalString imple
         opposite = r;
     }
 
+    /**
+     * Returns the constant for the given  {@code PARAM_SIGN_REVERSAL} value.
+     *
+     * @param  value  the  {@code PARAM_SIGN_REVERSAL} value, potentially 
{@code null}.
+     * @return {@link #OPPOSITE} if {@code value} is true, {@link #SAME} if 
false, or {@code null} if null.
+     */
+    public static SignReversalComment of(final Boolean value) {
+        return (value == null) ? null : value ? OPPOSITE : SAME;
+    }
+
     /**
      * Returns a human-readable text for this constant.
      *

Reply via email to