This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 02fc9f92a27c1afbc5d1f9201da47732eb102325
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Apr 18 10:58:07 2026 +0200

    Bug fixes and improvements in the GeoTIFF writer.
    - Fix a "cannot write" exception when the operation method was not an 
instance of `AbstractMethod`.
    - Fix the test failure when the project was built in an environment without 
connection to an EPSG database.
    - Write both the EPSG codes and the CRS elements for helping applications 
that do not have access to an EPSG database.
      Before this commit, the writer omitted the CRS elements if EPSG codes 
were present for avoiding duplication.
---
 .../internal/shared/CoordinateOperations.java      |  25 ++++++++++++++++--
 .../referencing/operation/provider/EPSGName.java   |   4 +--
 .../apache/sis/referencing/crs/HardCodedCRS.java   |  10 +++++++-
 .../sis/referencing/datum/GeodeticDatumMock.java   |   4 ++-
 .../sis/referencing/datum/HardCodedDatum.java      |  21 +++++++++++++++-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |  28 +++++++++++++--------
 .../sis/storage/geotiff/GeoTiffStoreTest.java      |  25 +++++++++---------
 .../test/org/apache/sis/storage/geotiff/tiled.tiff | Bin 3564 -> 3882 bytes
 .../org/apache/sis/storage/geotiff/untiled.tiff    | Bin 2284 -> 2602 bytes
 9 files changed, 88 insertions(+), 29 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
index 83640f532b..22d9716c6b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/CoordinateOperations.java
@@ -41,6 +41,8 @@ import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
 import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+import org.apache.sis.referencing.operation.transform.MathTransformProvider;
+import org.apache.sis.referencing.operation.provider.AbstractProvider;
 import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.metadata.internal.shared.NameToIdentifier;
@@ -121,7 +123,7 @@ public final class CoordinateOperations {
      * @param  csFactory   the factory to use if the operation factory needs 
to create CS for intermediate steps.
      * @return the coordinate operation factory to use.
      */
-    public static CoordinateOperationFactory 
getCoordinateOperationFactory(Map<String,?> properties,
+    public static CoordinateOperationFactory 
getCoordinateOperationFactory(Map<String, ?> properties,
             final MathTransformFactory mtFactory, final CRSFactory crsFactory, 
final CSFactory csFactory)
     {
         if (Containers.isNullOrEmpty(properties)) {
@@ -153,6 +155,25 @@ public final class CoordinateOperations {
         return null;
     }
 
+    /**
+     * Converts the given operation method to one of the predefined methods 
known to Apache <abbr>SIS</abbr>.
+     * This function can be used for replacing an operation method created 
from the <abbr>EPSG</abbr> database
+     * by an hard-coded operation method implementing the {@link 
MathTransformProvider} interface.
+     * This is needed for example for getting authority codes of 
non-<abbr>EPSG</abbr> authorities.
+     *
+     * @param  method  the method to convert to a predefined method if 
possible.
+     * @return the predefined method, or {@code method} if there is no 
predefined method.
+     */
+    public static OperationMethod toPredefinedMethod(final OperationMethod 
method) {
+        if (!(method instanceof AbstractProvider)) try {
+            return findMethod(DefaultMathTransformFactory.provider(), method);
+        } catch (NoSuchIdentifierException e) {
+            // Source class and method name will be inferred from the stack 
trace.
+            Logging.recoverableException(LOGGER, null, null, e);
+        }
+        return method;
+    }
+
     /**
      * Returns the operation method for the name of the given identified 
object.
      * The given object is typically a {@code ParameterDescriptorGroup}.
@@ -171,7 +192,7 @@ public final class CoordinateOperations {
             methodIdentifier = methodName;
         }
         /*
-         * Get the MathTransformProvider of the same name or identifier than 
the given parameter group.
+         * Get the MathTransformProvider of the same name or identifier as the 
given parameter group.
          * We give precedence to EPSG identifier because operation method 
names are sometimes ambiguous
          * (e.g. "Lambert Azimuthal Equal Area (Spherical)"). If we fail to 
find the method by its EPSG code,
          * we will try searching by method name. As a side effect, this second 
attempt will produce a better
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
index bd3d4b6f8f..7965a3dc62 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
@@ -93,7 +93,7 @@ public final class EPSGName {  // TODO: consider extending 
NamedIdentifier if we
      * @param  nameOGC     the OGC name, or {@code null} if none.
      * @return a map of properties for building the operation method.
      */
-    public static Map<String,Object> properties(final int identifier, final 
String name, final String nameOGC) {
+    public static Map<String, Object> properties(final int identifier, final 
String name, final String nameOGC) {
         return properties(identifier, name, (nameOGC == null) ? null :
             // Version and remarks are intentionally null here, since they are 
not EPSG version or remarks.
             new NamedIdentifier(Citations.OGC, Constants.OGC, nameOGC, null, 
null));
@@ -108,7 +108,7 @@ public final class EPSGName {  // TODO: consider extending 
NamedIdentifier if we
      * @param  nameOGC     the OGC name, or {@code null} if none.
      * @return a map of properties for building the operation method.
      */
-    public static Map<String,Object> properties(final int identifier, final 
String name, final GenericName nameOGC) {
+    public static Map<String, Object> properties(final int identifier, final 
String name, final GenericName nameOGC) {
         final Map<String,Object> properties = new HashMap<>(4);
         properties.put(IdentifiedObject.IDENTIFIERS_KEY, 
identifier(identifier));
         properties.put(IdentifiedObject.NAME_KEY, create(name));
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/HardCodedCRS.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/HardCodedCRS.java
index f0949b31d8..7070101bab 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/HardCodedCRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/crs/HardCodedCRS.java
@@ -327,11 +327,19 @@ public final class HardCodedCRS {
     public static final DefaultImageCRS IMAGE = new DefaultImageCRS(
             getProperties(HardCodedDatum.IMAGE), HardCodedDatum.IMAGE, 
HardCodedCS.GRID);
 
+    /**
+     * A two-dimensional geographic coordinate reference system for Jupiter.
+     * This CRS uses (<var>longitude</var>, <var>latitude</var>) coordinates.
+     * The angular units are decimal degrees and the prime meridian is 
"Reference Meridian".
+     */
+    public static final DefaultGeographicCRS JUPITER = new 
DefaultGeographicCRS(
+            getProperties(HardCodedDatum.JUPITER), HardCodedDatum.JUPITER, 
null, HardCodedCS.GEODETIC_2D);
+
     /**
      * Creates a map of properties for the given name and code with world 
extent.
      */
     private static Map<String,?> properties(final String name, final String 
code) {
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String, Object>(4);
         properties.put(NAME_KEY, name);
         properties.put(DOMAIN_OF_VALIDITY_KEY, Extents.WORLD);
         if (code != null) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/GeodeticDatumMock.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/GeodeticDatumMock.java
index e32b5b23fc..b5ec555cda 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/GeodeticDatumMock.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/GeodeticDatumMock.java
@@ -32,8 +32,10 @@ import org.apache.sis.test.mock.IdentifiedObjectMock;
  * A dummy implementation of {@link GeodeticDatum}, which is also its own 
ellipsoid.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see HardCodedDatum
  */
-@SuppressWarnings("serial")
+@SuppressWarnings({"serial", "exports"})
 public final class GeodeticDatumMock extends IdentifiedObjectMock implements 
GeodeticDatum, Ellipsoid {
     /**
      * The "GRS 1980" datum. This is very similar to {@link #WGS84}.
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
index b1d86a1c5f..32dabfaef9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/datum/HardCodedDatum.java
@@ -38,8 +38,18 @@ import static org.opengis.referencing.ObjectDomain.*;
  * Collection of datum for testing purpose.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see GeodeticDatumMock
  */
 public final class HardCodedDatum {
+    /**
+     * Reference meridian, with angular measurements in decimal degrees.
+     * This is preferred to {@link #GREENWICH} for planets other than Earth.
+     */
+    public static final DefaultPrimeMeridian REFERENCE = new 
DefaultPrimeMeridian(
+            properties("Reference Meridian", null, null),
+            0, Units.DEGREE);
+
     /**
      * Greenwich meridian (EPSG:8901), with angular measurements in decimal 
degrees.
      */
@@ -115,6 +125,15 @@ public final class HardCodedDatum {
             properties("Not specified (based on GRS 1980 Authalic Sphere)", 
"6047", "Not a valid datum."),
             new DefaultEllipsoid(GeodeticDatumMock.SPHERE.getEllipsoid()), 
GREENWICH);
 
+    /**
+     * The datum of Jupiter (2015). Its ellipsoid has a high eccentricity, 
higher than
+     * the eccentricity for which the formulas of many map projections were 
designed for.
+     */
+    public static final DefaultGeodeticDatum JUPITER = new 
DefaultGeodeticDatum(
+            properties("Jupiter (2015)", null, null),
+            DefaultEllipsoid.createEllipsoid(properties("Jupiter (2015)", 
null, null),
+                    71492000, 66854000, Units.METRE), REFERENCE);
+
     /**
      * Ellipsoid for measurements of height above the ellipsoid.
      * This is not a valid datum according ISO 19111, but is used by Apache 
SIS for internal calculation.
@@ -174,7 +193,7 @@ public final class HardCodedDatum {
      * @param  scope  the object scope, or {@code null} if none.
      */
     private static Map<String,?> properties(final String name, final String 
code, final CharSequence scope) {
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String, Object>(4);
         properties.put(NAME_KEY, name);
         if (code != null) {
             properties.put(IDENTIFIERS_KEY, new 
NamedIdentifier(HardCodedCitations.EPSG, code));
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
index e13ced791d..50047389e1 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
@@ -72,6 +72,7 @@ import 
org.apache.sis.referencing.operation.provider.MapProjection;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
 import org.apache.sis.referencing.internal.shared.AxisDirections;
+import org.apache.sis.referencing.internal.shared.CoordinateOperations;
 import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
 import org.apache.sis.referencing.internal.shared.WKTKeywords;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -114,6 +115,13 @@ public final class GeoEncoder {
      */
     private static final int MATRIX_SIZE = 4;
 
+    /**
+     * Whether to write both the <abbr>EPSG</abbr> code and the 
<abbr>CRS</abbr> definition.
+     * Writing both forms is redundant, but may help software that have no 
<abbr>EPSG</abbr> data.
+     * It does not seem worth to make this option configurable by users for 
now.
+     */
+    private static final boolean REDUNDANT = true;
+
     /**
      * The listeners where to report warnings.
      */
@@ -234,9 +242,9 @@ public final class GeoEncoder {
     private int citationLengthIndex;
 
     /**
-     * Whether to disable attempts to write EPSG codes. This is set to {@code 
true} on the first attempt to use the
-     * EPSG database if it appears to be unavailable. This is used for 
avoiding many retries which will continue to
-     * fail.
+     * Whether to disable attempts to write <abbr>EPSG</abbr> codes. This is 
set to {@code true} on the first
+     * failed attempt to use the <abbr>EPSG</abbr> database. This is used for 
avoiding many retries which will
+     * continue to fail.
      */
     private boolean disableEPSG;
 
@@ -460,7 +468,7 @@ public final class GeoEncoder {
             throw unsupportedType(cs);
         }
         /*
-         * Start writing GeoTIFF keys for the geodetic CRS, potentially
+         * Start writing GeoTIFF keys for the geodetic CRS.
          */
         writeModelType(isBaseCRS ? GeoCodes.ModelTypeProjected : type);
         if (writeEPSG(GeoKeys.GeodeticCRS, crs)) {
@@ -475,7 +483,7 @@ public final class GeoEncoder {
      * Writes entries for the geodetic datum, followed by prime meridian and 
ellipsoid in that order.
      * The order matter because GeoTIFF specification requires keys to be 
sorted in increasing order.
      * A difficulty is that units of measurement are between prime meridian 
and ellipsoid,
-     * and the angular unit is needed for projected CRS too.
+     * and the angular unit is needed for projected <abbr>CRS</abbr> too.
      * This is handled by storing units in the {@link #units} map.
      *
      * @param  datum  the datum to write.
@@ -551,17 +559,17 @@ public final class GeoEncoder {
     private boolean writeCRS(final ProjectedCRS crs)
             throws FactoryException, IncommensurableException, 
IncompatibleResourceException
     {
-        final boolean previous = disableEPSG;
         final Conversion projection = crs.getConversionFromBase();
-        OperationMethod method = projection.getMethod();
+        OperationMethod method = 
CoordinateOperations.toPredefinedMethod(projection.getMethod());
         if (method instanceof MapProjection) {
             isPseudoProjection = !method.equals(method = ((MapProjection) 
method).sourceOfPseudo());
-            disableEPSG = isPseudoProjection;
         }
         /*
          * Write the base CRS only after `isPseudoProjection` has been 
determined,
          * because it changes the way to write the datum and the ellipsoid.
          */
+        final boolean previous = disableEPSG;
+        disableEPSG = isPseudoProjection;
         writeCRS(crs.getBaseCRS(), true);
         disableEPSG = previous;
         if (writeEPSG(GeoKeys.ProjectedCRS, crs)) {
@@ -753,7 +761,7 @@ public final class GeoEncoder {
     }
 
     /**
-     * Writes the EPSG code of the given object, or {@value 
GeoCodes#userDefined} if none.
+     * Writes the <abbr>EPSG</abbr> code of the given object, or {@value 
GeoCodes#userDefined} if none.
      * Returns whether the caller should write user-defined object in 
replacement or in addition to EPSG code.
      *
      * @param  key     the numeric identifier of the GeoTIFF key.
@@ -783,7 +791,7 @@ public final class GeoEncoder {
             disableEPSG = true;
         }
         writeShort(key, epsg);
-        return (epsg == GeoCodes.userDefined);
+        return REDUNDANT || (epsg == GeoCodes.userDefined);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
index 51aae148cc..fd5fe04b1e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
@@ -134,26 +134,22 @@ public final class GeoTiffStoreTest extends TestCase {
     /**
      * Writes an image and compare with the {@code "untiled.tiff"} file.
      *
-     * @throws TransformException if an error occurred while computing the 
domain of the image.
-     * @throws DataStoreException if an error occurred while writing the 
GeoTIFF file.
-     * @throws IOException if an error occurred while reading the file of 
expected content.
+     * @throws Exception if a referencing or I/O error occurred.
      */
     @Test
-    public void testWriteUntiled() throws TransformException, 
DataStoreException, IOException {
-        testWrite(UNTILED, new Rectangle(32, 16), null, 2284);
+    public void testWriteUntiled() throws Exception {
+        testWrite(UNTILED, new Rectangle(32, 16), null, 2602);
     }
 
     /**
      * Writes an image and compare with the {@code "tiled.tiff"} file.
      *
-     * @throws TransformException if an error occurred while computing the 
domain of the image.
-     * @throws DataStoreException if an error occurred while writing the 
GeoTIFF file.
-     * @throws IOException if an error occurred while reading the file of 
expected content.
+     * @throws Exception if a referencing or I/O error occurred.
      */
     @Test
-    public void testWriteTiled() throws TransformException, 
DataStoreException, IOException {
+    public void testWriteTiled() throws Exception {
         final var tileSize = new Dimension(16, 16);     // TIFF tile size must 
be multiple of 16.
-        testWrite(TILED, new Rectangle(tileSize.width * 3, tileSize.height * 
2), tileSize, 3564);
+        testWrite(TILED, new Rectangle(tileSize.width * 3, tileSize.height * 
2), tileSize, 3882);
     }
 
     /**
@@ -167,11 +163,16 @@ public final class GeoTiffStoreTest extends TestCase {
     private static void testWrite(final String filename, final Rectangle 
bounds, final Dimension tileSize, final int length)
             throws TransformException, DataStoreException, IOException
     {
-        var geographicArea = new GeneralEnvelope(HardCodedCRS.WGS84);
+        /*
+         * We need a CRS which has no EPSG code for ensuring that the test 
write the same GeoTIFF keys
+         * with or without the presence of an EPSG database on machine which 
is building this project.
+         */
+        var crs = HardCodedConversions.mercator(HardCodedCRS.JUPITER);
+        var geographicArea = new GeneralEnvelope(HardCodedCRS.JUPITER);
         geographicArea.setRange(0, 132, 145);   // Range of longitude values.
         geographicArea.setRange(1,  30,  42);   // Range of latitude values.
         final GridCoverage coverage = new GridCoverageBuilder()
-                .setDomain(Envelopes.transform(geographicArea, 
HardCodedConversions.mercator()))
+                .setDomain(Envelopes.transform(geographicArea, crs))
                 .setIntegerValues(DataType.BYTE, bounds, tileSize, (x, y) -> 
100 * y + x)
                 .flipGridAxis(1)
                 .build();
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
index fb34ea58d7..68195c9260 100644
Binary files 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
 and 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
 differ
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff
index 814b7de4c7..451287f5cf 100644
Binary files 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff
 and 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff
 differ

Reply via email to