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

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

commit f63533fbb6dcec6676ea102a0cacf03c4506a8a9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jan 24 15:54:40 2025 +0100

    Improves the encoding of map projection in GeoTIFF files:
    * Fix the missing EPSG code for prime meridian.
    * Replace "Pseudo-Sinusoidal" by "Sinusoidal".
---
 .../org/apache/sis/metadata/privy/Identifiers.java |   2 +-
 .../sis/referencing/ImmutableIdentifier.java       | 131 +++++++++---------
 .../apache/sis/referencing/NamedIdentifier.java    |  24 ++--
 .../sis/referencing/StandardDefinitions.java       |  14 +-
 .../operation/provider/MapProjection.java          |  19 ++-
 .../operation/provider/PseudoMercator.java         |  10 ++
 .../operation/provider/PseudoSinusoidal.java       |  10 ++
 .../sis/storage/geotiff/writer/GeoEncoder.java     | 147 +++++++++++++++------
 8 files changed, 222 insertions(+), 135 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
index 13a42017ea..61e52aad6b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/Identifiers.java
@@ -51,7 +51,7 @@ public final class Identifiers extends Static {
      * (ignoring case). This particular combination of code and codespace is 
handled in a special way.
      *
      * <p>This method can be used for identifying where in Apache SIS source 
code the relationship between
-     * EPSG authority and IOGP code space is hard-coded.</p>
+     * <abbr>EPSG</abbr> authority and <abbr>IOGP</abbr> code space is 
hard-coded.</p>
      *
      * @param  codeSpace  the identifier code space, or {@code null}.
      * @param  code       the identifier code, or {@code null}.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ImmutableIdentifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ImmutableIdentifier.java
index 491c9742a5..a66b32941c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ImmutableIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/ImmutableIdentifier.java
@@ -100,7 +100,7 @@ import static 
org.apache.sis.util.collection.Containers.property;
  * </li></ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.5
  *
  * @see org.apache.sis.metadata.iso.DefaultIdentifier
  * @see org.apache.sis.referencing.IdentifiedObjects#toURN(Class, Identifier)
@@ -215,33 +215,27 @@ public class ImmutableIdentifier extends 
FormattableObject implements Identifier
      *     <th>Property name</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#CODE_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getCode()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#CODESPACE_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getCodeSpace()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#AUTHORITY_KEY}</td>
      *     <td>{@link String} or {@link Citation}</td>
      *     <td>{@link #getAuthority()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#VERSION_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getVersion()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#DESCRIPTION_KEY}</td>
      *     <td>{@link String} or {@link InternationalString}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.referencing.AbstractIdentifiedObject#LOCALE_KEY}</td>
      *     <td>{@link Locale}</td>
      *     <td>(none)</td>
@@ -461,8 +455,9 @@ public class ImmutableIdentifier extends FormattableObject 
implements Identifier
      * @see <a 
href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#33";>WKT 2 
specification §7.3.4</a>
      */
     @Override
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     protected String formatTo(final Formatter formatter) {
-        String keyword = null;
+        final String keyword;
         /*
          * The code, codeSpace, authority and version local variables in this 
method usually have the exact same
          * value than the fields of the same name in this class.  But we get 
those values by invoking the public
@@ -471,61 +466,61 @@ public class ImmutableIdentifier extends 
FormattableObject implements Identifier
          * than using the public methods.
          */
         final String code = getCode();
-        if (code != null) {
-            final String   codeSpace = getCodeSpace();
-            final Citation authority = getAuthority();
-            final String cs = (codeSpace != null) ? codeSpace : 
Identifiers.getIdentifier(authority, true);
-            if (cs != null) {
-                final Convention convention = formatter.getConvention();
-                if (convention.majorVersion() == 1) {
-                    keyword = WKTKeywords.Authority;
-                    formatter.append(cs,   ElementKind.IDENTIFIER);
-                    formatter.append(code, ElementKind.IDENTIFIER);
-                } else {
-                    keyword = WKTKeywords.Id;
-                    formatter.append(cs, ElementKind.IDENTIFIER);
-                    appendCode(formatter, code);
-                    final String version = getVersion();
-                    if (version != null) {
-                        appendCode(formatter, version);
-                    }
-                    /*
-                     * In order to simplify the WKT, format the citation only 
if it is different than the code space.
-                     * We will also omit the citation if this identifier is 
for a parameter value, because parameter
-                     * values are handled in a special way by the 
international standard:
-                     *
-                     *   - ISO 19162 explicitly said that we shall format the 
identifier for the root element only,
-                     *     and omit the identifier for all inner elements 
EXCEPT parameter values and operation method.
-                     *   - Exclusion of identifier for inner elements is 
performed by the Formatter class, so it does
-                     *     not need to be checked here.
-                     *   - Parameter values are numerous, while operation 
methods typically appear only once in a WKT
-                     *     document. So we will simplify the parameter values 
only (not the operation methods) except
-                     *     if the parameter value is the root element (in 
which case we will format full identifier).
-                     */
-                    final FormattableObject enclosing = 
formatter.getEnclosingElement(1);
-                    final boolean              isRoot = 
formatter.getEnclosingElement(2) == null;
-                    if (isRoot || !(enclosing instanceof ParameterValue<?>)) {
-                        final String citation = 
Identifiers.getIdentifier(authority, false);
-                        if (citation != null && !citation.equals(cs)) {
-                            formatter.append(new Cite(citation));
-                        }
-                    }
-                    /*
-                     * Do not format the optional URI element for internal 
convention,
-                     * because this property is currently computed rather than 
stored.
-                     * Other conventions format only for the ID[…] of root 
element.
-                     */
-                    if (isRoot && enclosing != null && convention != 
Convention.INTERNAL) {
-                        final String urn = 
NameMeaning.toURN(enclosing.getClass(), cs, version, code);
-                        if (urn != null) {
-                            formatter.append(new FormattableObject() {
-                                @Override protected String formatTo(final 
Formatter formatter) {
-                                    formatter.append(urn, null);
-                                    return WKTKeywords.URI;
-                                }
-                            });
+        String codeSpace = getCodeSpace();
+        final Citation authority = getAuthority();
+        if (codeSpace == null) {
+            codeSpace = Identifiers.getIdentifier(authority, true);
+        }
+        if (code == null || codeSpace == null) {
+            formatter.setInvalidWKT(getClass(), null);
+        }
+        formatter.append(codeSpace, ElementKind.IDENTIFIER);
+        final Convention convention = formatter.getConvention();
+        if (convention.majorVersion() == 1) {
+            keyword = WKTKeywords.Authority;
+            formatter.append(code, ElementKind.IDENTIFIER);
+        } else {
+            keyword = WKTKeywords.Id;
+            appendCode(formatter, code);
+            final String version = getVersion();
+            if (version != null) {
+                appendCode(formatter, version);
+            }
+            /*
+             * In order to simplify the WKT, format the citation only if it is 
different than the code space.
+             * We will also omit the citation if this identifier is for a 
parameter value, because parameter
+             * values are handled in a special way by the international 
standard:
+             *
+             *   - ISO 19162 explicitly said that we shall format the 
identifier for the root element only,
+             *     and omit the identifier for all inner elements EXCEPT 
parameter values and operation method.
+             *   - Exclusion of identifier for inner elements is performed by 
the Formatter class, so it does
+             *     not need to be checked here.
+             *   - Parameter values are numerous, while operation methods 
typically appear only once in a WKT
+             *     document. So we will simplify the parameter values only 
(not the operation methods) except
+             *     if the parameter value is the root element (in which case 
we will format full identifier).
+             */
+            final FormattableObject enclosing = 
formatter.getEnclosingElement(1);
+            final boolean              isRoot = 
formatter.getEnclosingElement(2) == null;
+            if (isRoot || !(enclosing instanceof ParameterValue<?>)) {
+                final String citation = Identifiers.getIdentifier(authority, 
false);
+                if (citation != null && !citation.equals(codeSpace)) {
+                    formatter.append(new Cite(citation));
+                }
+            }
+            /*
+             * Do not format the optional URI element for internal convention,
+             * because this property is currently computed rather than stored.
+             * Other conventions format only for the ID[…] of root element.
+             */
+            if (isRoot && enclosing != null && convention != 
Convention.INTERNAL) {
+                final String urn = NameMeaning.toURN(enclosing.getClass(), 
codeSpace, version, code);
+                if (urn != null) {
+                    formatter.append(new FormattableObject() {
+                        @Override protected String formatTo(final Formatter 
formatter) {
+                            formatter.append(urn, null);
+                            return WKTKeywords.URI;
                         }
-                    }
+                    });
                 }
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
index 604c7c5e0d..a80def7661 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
@@ -155,41 +155,33 @@ public class NamedIdentifier extends ImmutableIdentifier 
implements GenericName
      *     <th>Property name</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@code "name"}</td>
      *     <td>{@link GenericName}</td>
      *     <td>(none)</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <th colspan="3" class="hsep">Defined in parent class (reminder)</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#CODE_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getCode()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#CODESPACE_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getCodeSpace()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#AUTHORITY_KEY}</td>
      *     <td>{@link String} or {@link Citation}</td>
      *     <td>{@link #getAuthority()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#VERSION_KEY}</td>
      *     <td>{@link String}</td>
      *     <td>{@link #getVersion()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#DESCRIPTION_KEY}</td>
      *     <td>{@link String} or {@link InternationalString}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.referencing.AbstractIdentifiedObject#LOCALE_KEY}</td>
      *     <td>{@link Locale}</td>
      *     <td>(none)</td>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
index cac8d9d285..ac942698a4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/StandardDefinitions.java
@@ -119,9 +119,9 @@ final class StandardDefinitions {
      */
     static final Citation AUTHORITY;
     static {
-        final DefaultCitation c = new DefaultCitation();
+        final var c = new DefaultCitation();
         c.setTitle(Vocabulary.formatInternational(Vocabulary.Keys.SubsetOf_1, 
Constants.EPSG));
-        c.setEdition(new 
SimpleInternationalString(StandardDefinitions.VERSION));
+        c.setEdition(new SimpleInternationalString(VERSION));
         c.getPresentationForms().add(PresentationForm.DOCUMENT_DIGITAL);
         c.getOtherCitationDetails().add(NOTICE);
         c.transitionTo(DefaultCitation.State.FINAL);
@@ -144,7 +144,7 @@ final class StandardDefinitions {
      * @return the map of properties to give to constructors or factory 
methods.
      */
     private static Map<String,Object> properties(final int code, final String 
name, final String alias, final boolean world) {
-        final Map<String,Object> map = new HashMap<>(8);
+        final var map = new HashMap<String,Object>(8);
         if (code != 0) {
             map.put(IDENTIFIERS_KEY, new NamedIdentifier(AUTHORITY, 
Constants.EPSG, String.valueOf(code), VERSION, null));
         }
@@ -166,7 +166,7 @@ final class StandardDefinitions {
     private static void addWMS(final Map<String,Object> properties, final 
String code) {
         properties.put(IDENTIFIERS_KEY, new NamedIdentifier[] {
             (NamedIdentifier) properties.get(IDENTIFIERS_KEY),
-            new NamedIdentifier(Citations.WMS, code)
+            new NamedIdentifier(Citations.WMS, Constants.OGC, code, null, null)
         });
     }
 
@@ -196,7 +196,7 @@ final class StandardDefinitions {
         final ParameterValueGroup parameters = 
method.getParameters().createValue();
         String name = isUTM ? 
TransverseMercator.Zoner.UTM.setParameters(parameters, latitude, longitude)
                             : PolarStereographicA.setParameters(parameters, 
latitude >= 0);
-        final DefaultConversion conversion = new 
DefaultConversion(properties(0, name, null, false), method, null, parameters);
+        final var conversion = new DefaultConversion(properties(0, name, null, 
false), method, null, parameters);
 
         name = baseCRS.getName().getCode() + " / " + name;
         return new DefaultProjectedCRS(properties(code, name, null, false), 
baseCRS, conversion, derivedCS);
@@ -289,9 +289,9 @@ final class StandardDefinitions {
      * If another prime meridian is desired, the EPSG database shall be used.
      */
     static PrimeMeridian primeMeridian() {
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String,Object>(4);
         properties.put(NAME_KEY, new NamedIdentifier(AUTHORITY, "Greenwich")); 
         // Name is fixed by ISO 19111.
-        properties.put(IDENTIFIERS_KEY, new NamedIdentifier(AUTHORITY, 
GREENWICH));
+        properties.put(IDENTIFIERS_KEY, new NamedIdentifier(AUTHORITY, 
Constants.EPSG, GREENWICH, VERSION, null));
         return new DefaultPrimeMeridian(properties, 0, Units.DEGREE);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/MapProjection.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/MapProjection.java
index d90b656e6e..e988f87515 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/MapProjection.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/MapProjection.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.operation.provider;
 
-import java.util.Map;
 import java.util.HashMap;
 import java.util.NoSuchElementException;
 import jakarta.xml.bind.annotation.XmlTransient;
@@ -139,7 +138,7 @@ public abstract class MapProjection extends 
AbstractProvider {
             new NamedIdentifier(Citations.GEOTIFF, "SemiMajorAxis"),
             new NamedIdentifier(Citations.PROJ4,   "a")
         };
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String,Object>(4);
         properties.put(AUTHORITY_KEY,  Citations.OGC);
         properties.put(NAME_KEY,       Constants.SEMI_MAJOR);
         properties.put(ALIAS_KEY,      aliases);
@@ -179,6 +178,22 @@ public abstract class MapProjection extends 
AbstractProvider {
               (byte) 2);
     }
 
+    /**
+     * If this map projection is a pseudo-projection, returns the non-pseudo 
variant.
+     * Otherwise, returns {@code this}.
+     *
+     * <h4>Purpose</h4>
+     * Some formats such as GeoTIFF supports only a hard-coded list of map 
projection methods.
+     * GeoTIFF has a code for {@link Sinusoidal} but none for {@link 
PseudoSinusoidal}.
+     * Therefore, the writer needs to replace the latter by the former with 
adjustment
+     * of the ellipsoid axis lengths.
+     *
+     * @return the non-pseudo variant of this map projection, or {@code this} 
if none.
+     */
+    public MapProjection sourceOfPseudo() {
+        return this;
+    }
+
     /**
      * Validates the given parameter value. This method duplicates the 
verification already
      * done by {@link 
org.apache.sis.parameter.DefaultParameterValue#setValue(Object, Unit)}.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoMercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoMercator.java
index a0f1762258..c1d377690e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoMercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoMercator.java
@@ -56,4 +56,14 @@ public final class PseudoMercator extends AbstractMercator {
     public PseudoMercator() {
         super(PARAMETERS);
     }
+
+    /**
+     * Returns the non-pseudo variant of this map projection.
+     *
+     * @return the non-pseudo variant of this map projection.
+     */
+    @Override
+    public MapProjection sourceOfPseudo() {
+        return new Mercator1SP();
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoSinusoidal.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoSinusoidal.java
index dedb47d2e2..15d8e69bd0 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoSinusoidal.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PseudoSinusoidal.java
@@ -53,4 +53,14 @@ public final class PseudoSinusoidal extends Sinusoidal {
     public PseudoSinusoidal() {
         super(parameters());
     }
+
+    /**
+     * Returns the non-pseudo variant of this map projection.
+     *
+     * @return the non-pseudo variant of this map projection.
+     */
+    @Override
+    public MapProjection sourceOfPseudo() {
+        return new Sinusoidal();
+    }
 }
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 82370219a2..3f9f551ae3 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
@@ -46,6 +46,7 @@ import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.VerticalDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.parameter.GeneralParameterValue;
 import org.opengis.parameter.ParameterValue;
@@ -59,6 +60,7 @@ import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.datum.PseudoDatum;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+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.privy.AxisDirections;
@@ -105,11 +107,21 @@ public final class GeoEncoder {
     private final StoreListeners listeners;
 
     /**
-     * Overall configuration of the GeoTIFF file, or {@code null} if none.
+     * Overall description of the GeoTIFF file, or {@code null} if none.
      * This is the value to store in {@link GeoKeys#Citation}.
      */
     private String citation;
 
+    /**
+     * Whether the map projection is a pseudo-projection. The latter has no 
GeoTIFF code.
+     * Therefore, they need to be replaced by the non-pseudo variant with 
adjustments of
+     * object names and ellipsoid axis lengths.
+     *
+     * @see MapProjection#sourceOfPseudo()
+     * @see #writeEllipsoid(Ellipsoid)
+     */
+    private boolean isPseudoProjection;
+
     /**
      * The axis directions of the grid geometry, or {@code null} if none.
      * The array length is 2 or 3, with the vertical axis always last.
@@ -369,51 +381,85 @@ public final class GeoEncoder {
             throw unsupportedType(cs);
         }
         /*
-         * Start writing GeoTIFF keys for the geodetic CRS, potentially 
followed by datum, 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.
+         * Start writing GeoTIFF keys for the geodetic CRS, potentially
          */
         writeModelType(isBaseCRS ? GeoCodes.ModelTypeProjected : type);
         if (writeEPSG(GeoKeys.GeodeticCRS, crs)) {
-            writeName(GeoKeys.GeodeticCitation, "GCS Name", crs);
-            final GeodeticDatum  datum = PseudoDatum.of(crs);
-            if (writeEPSG(GeoKeys.GeodeticDatum, datum)) {
-                appendName(WKTKeywords.Datum, datum);
-                final PrimeMeridian primem = datum.getPrimeMeridian();
-                final double longitude;
-                if (writeEPSG(GeoKeys.PrimeMeridian, primem)) {
-                    appendName(WKTKeywords.PrimeM, datum);
-                    longitude = primem.getGreenwichLongitude();
-                } else {
-                    longitude = 0;                                      // 
Means "do not write prime meridian".
-                }
-                final Ellipsoid     ellipsoid  = datum.getEllipsoid();
-                final Unit<Length>  axisUnit   = ellipsoid.getAxisUnit();
-                final Unit<?>       linearUnit = 
units.putIfAbsent(UnitKey.LINEAR, axisUnit);
-                final UnitConverter toLinear   = 
axisUnit.getConverterToAny(linearUnit != null ? linearUnit : axisUnit);
-                writeUnit(UnitKey.LINEAR);     // Must be after the `units` 
map have been updated.
-                writeUnit(UnitKey.ANGULAR);
-                if (writeEPSG(GeoKeys.Ellipsoid, ellipsoid)) {
-                    appendName(WKTKeywords.Ellipsoid, ellipsoid);
-                    writeDouble(GeoKeys.SemiMajorAxis, 
toLinear.convert(ellipsoid.getSemiMajorAxis()));
-                    if (ellipsoid.isSphere() || !ellipsoid.isIvfDefinitive()) {
-                        writeDouble(GeoKeys.SemiMinorAxis, 
toLinear.convert(ellipsoid.getSemiMinorAxis()));
-                    } else {
-                        writeDouble(GeoKeys.InvFlattening, 
ellipsoid.getInverseFlattening());
-                    }
-                }
-                if (longitude != 0) {
-                    Unit<Angle> unit = primem.getAngularUnit();
-                    UnitConverter c = 
unit.getConverterToAny(units.getOrDefault(UnitKey.ANGULAR, Units.DEGREE));
-                    writeDouble(GeoKeys.PrimeMeridianLongitude, 
c.convert(longitude));
-                }
-            }
+            writeName(GeoKeys.GeodeticCitation, "GCS Name", isPseudoProjection 
? null : crs);
+            writeDatum(PseudoDatum.of(crs));
         } else if (isBaseCRS) {
             writeUnit(UnitKey.ANGULAR);         // Map projection parameters 
may need this unit.
         }
     }
 
+    /**
+     * 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.
+     * This is handled by storing units in the {@link #units} map.
+     *
+     * @param  datum  the datum to write.
+     * @throws FactoryException if an error occurred while fetching an EPSG 
code.
+     * @throws IncommensurableException if a measure uses an unexpected unit 
of measurement.
+     * @throws IncompatibleResourceException if the datum has an incompatible 
property.
+     */
+    private void writeDatum(final GeodeticDatum datum)
+            throws FactoryException, IncommensurableException, 
IncompatibleResourceException
+    {
+        if (writeEPSG(GeoKeys.GeodeticDatum, datum)) {
+            appendName(WKTKeywords.Datum, datum);
+            final boolean previous = disableEPSG;
+            disableEPSG &= !isPseudoProjection;     // Re-enable the use of 
EPSG codes for the prime meridian.
+
+            double longitude = 0;   // Means "do not write prime meridian".
+            final PrimeMeridian primem = datum.getPrimeMeridian();
+            if (writeEPSG(GeoKeys.PrimeMeridian, primem)) {
+                appendName(WKTKeywords.PrimeM, datum);
+                longitude = primem.getGreenwichLongitude();
+            }
+            disableEPSG = previous;
+            writeEllipsoid(datum.getEllipsoid());
+            if (longitude != 0) {
+                Unit<Angle> unit = primem.getAngularUnit();
+                UnitConverter c = 
unit.getConverterToAny(units.getOrDefault(UnitKey.ANGULAR, Units.DEGREE));
+                writeDouble(GeoKeys.PrimeMeridianLongitude, 
c.convert(longitude));
+            }
+        }
+    }
+
+    /**
+     * Writes the effective ellipsoid. "Effective" means that if a 
pseudo-projection is used,
+     * then the calculation uses a semi-minor axis length equals to the 
semi-major axis length.
+     *
+     * @param  ellipsoid  the ellipsoid to write.
+     * @throws FactoryException if an error occurred while fetching an EPSG 
code.
+     * @throws IncommensurableException if a measure uses an unexpected unit 
of measurement.
+     * @throws IncompatibleResourceException if the ellipsoid has an 
incompatible property.
+     */
+    private void writeEllipsoid(final Ellipsoid ellipsoid)
+            throws FactoryException, IncommensurableException, 
IncompatibleResourceException
+    {
+        final Unit<Length>  axisUnit   = ellipsoid.getAxisUnit();
+        final Unit<?>       linearUnit = units.putIfAbsent(UnitKey.LINEAR, 
axisUnit);
+        final UnitConverter toLinear   = axisUnit.getConverterToAny(linearUnit 
!= null ? linearUnit : axisUnit);
+        writeUnit(UnitKey.LINEAR);     // Must be after the `units` map has 
been updated.
+        writeUnit(UnitKey.ANGULAR);
+        if (writeEPSG(GeoKeys.Ellipsoid, ellipsoid)) {
+            appendName(WKTKeywords.Ellipsoid, ellipsoid);
+            double axisLength = toLinear.convert(ellipsoid.getSemiMajorAxis());
+            writeDouble(GeoKeys.SemiMajorAxis, axisLength);
+            if (!isPseudoProjection) {
+                if (ellipsoid.isIvfDefinitive() && !ellipsoid.isSphere()) {
+                    writeDouble(GeoKeys.InvFlattening, 
ellipsoid.getInverseFlattening());
+                    return;
+                }
+                axisLength = toLinear.convert(ellipsoid.getSemiMinorAxis());
+            }
+            writeDouble(GeoKeys.SemiMinorAxis, axisLength);
+        }
+    }
+
     /**
      * Writes entries for a projected CRS.
      * If the CRS is user-specified, then this method writes the geodetic CRS 
first.
@@ -426,13 +472,23 @@ 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();
+        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.
+         */
         writeCRS(crs.getBaseCRS(), true);
+        disableEPSG = previous;
         if (writeEPSG(GeoKeys.ProjectedCRS, crs)) {
             writeName(GeoKeys.ProjectedCitation, null, crs);
             addUnits(UnitKey.PROJECTED, crs.getCoordinateSystem());
-            final Conversion projection = crs.getConversionFromBase();
             if (writeEPSG(GeoKeys.Projection, projection)) {
-                final var method = projection.getMethod();
                 final short projCode = getGeoCode(0, method);
                 writeShort(GeoKeys.ProjMethod, projCode);
                 writeUnit(UnitKey.PROJECTED);
@@ -527,7 +583,7 @@ public final class GeoEncoder {
      *
      * @param  key     the numeric identifier of the GeoTIFF key.
      * @param  type    type of object for which to write the name, or {@code 
null} for no multiple-names citation.
-     * @param  object  the object for which to write the name.
+     * @param  object  the object for which to write the name, or {@code null} 
for an unnamed object.
      */
     private void writeName(final short key, final String type, final 
IdentifiedObject object) {
         String name = IdentifiedObjects.getName(object, null);
@@ -548,6 +604,15 @@ public final class GeoEncoder {
      * @param  object  the object for which to write the name.
      */
     private void appendName(final String type, final IdentifiedObject object) {
+        if (isPseudoProjection) {
+            /*
+             * The caller is writing a "Pseudo-Mercator" or 
"Pseudo-Sinusoidal" projection
+             * using the standard variant ("Mercator" or "Sinusoidal") but 
with a modified
+             * semi-minor axis length. Therefore, the ellipsoid name is no 
longer correct,
+             * and by consequence the datum name neither.
+             */
+            return;
+        }
         final String name = IdentifiedObjects.getName(object, null);
         if (name != null) {
             int i      = citationLengthIndex;

Reply via email to