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;
