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
