You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2025/08/20 17:46:56 UTC

(sis) branch geoapi-4.0 updated (b0791bfff2 -> b0f406a181)

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

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


    from b0791bfff2 Drop support of the `$SIS_DATA/Databases/ExternalSources` directory. This feature was used mostly for testing purposes, so this change has probably no incidence on users. In the unlikely case where this feature was used by someone in production, it would probably be broken anyway because this feature had an implicit execution of scripts that are impacted by the EPSG upgrade.
     new 9703e401f8 Fix the generation of embedded Derby database containing EPSG data. If the instruction documented in `.../epsg/README.md` are followed, then a build with tests enabled will automatically create the full spatial metadata database ready to deploy.
     new b0f406a181 Add "World Mercator" and "Pseudo-Mercator" in the set of CRS that are always present. There is a high demand for them, and they are needed for passing some tests anyway.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/sis/referencing/AuthorityFactories.java |  29 ++---
 .../main/org/apache/sis/referencing/CRS.java       |   2 +
 .../main/org/apache/sis/referencing/CommonCRS.java |  14 +--
 .../sis/referencing/EPSGFactoryFallback.java       |  41 ++++++--
 .../sis/referencing/StandardDefinitions.java       |  70 +++++++++---
 .../operation/provider/Mercator1SP.java            |   7 +-
 .../operation/provider/PolarStereographicA.java    |   2 +-
 .../operation/provider/PseudoMercator.java         |   7 +-
 .../operation/provider/TransverseMercator.java     |   2 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |   4 +-
 .../sis/referencing/EPSGFactoryFallbackTest.java   |   2 +-
 .../sis/referencing/StandardDefinitionsTest.java   |  29 +++++
 optional/build.gradle.kts                          |  18 ++--
 .../sis/resources/embedded/EmbeddedResources.java  |  14 +--
 .../sis/resources/embedded/package-info.java       |  12 +--
 .../resources/embedded/EmbeddedResourcesTest.java  |  40 ++++---
 .../apache/sis/resources/embedded/Generator.java   | 117 +++++++++++----------
 .../sis/referencing/factory/sql/epsg/README.md     |  13 +--
 .../factory/sql/epsg/ScriptProviderTest.java       |   4 +-
 19 files changed, 268 insertions(+), 159 deletions(-)


(sis) 01/02: Fix the generation of embedded Derby database containing EPSG data. If the instruction documented in `.../epsg/README.md` are followed, then a build with tests enabled will automatically create the full spatial metadata database ready to deploy.

Posted by de...@apache.org.
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 9703e401f8aeff168bcc0e04eca1757bb0886153
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 20 16:27:09 2025 +0200

    Fix the generation of embedded Derby database containing EPSG data.
    If the instruction documented in `.../epsg/README.md` are followed,
    then a build with tests enabled will automatically create the full
    spatial metadata database ready to deploy.
---
 optional/build.gradle.kts                          |  18 ++--
 .../sis/resources/embedded/EmbeddedResources.java  |  14 +--
 .../sis/resources/embedded/package-info.java       |  12 +--
 .../resources/embedded/EmbeddedResourcesTest.java  |  40 ++++---
 .../apache/sis/resources/embedded/Generator.java   | 117 +++++++++++----------
 .../sis/referencing/factory/sql/epsg/README.md     |  13 +--
 .../factory/sql/epsg/ScriptProviderTest.java       |   4 +-
 7 files changed, 112 insertions(+), 106 deletions(-)

diff --git a/optional/build.gradle.kts b/optional/build.gradle.kts
index 51e4111a76..cdc3818db3 100644
--- a/optional/build.gradle.kts
+++ b/optional/build.gradle.kts
@@ -175,13 +175,17 @@ fun downloadFontGIS() {
  * Adds symbolic links to EPSG license if those optional data are present.
  */
 fun addLicenseEPSG() {
-    var targetFile = File(file("build"), "classes/java/main/org.apache.sis.referencing.epsg/META-INF/LICENSE")
-    if (!targetFile.exists()) {
-        val sourceFile = File(file("src"), "org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/LICENSE.txt")
-        if (sourceFile.exists()) {
-            Files.createLink(targetFile.toPath(), sourceFile.toPath())
-            targetFile = File(file("build"), "classes/java/main/org.apache.sis.referencing.database/META-INF/LICENSE")
-            Files.createLink(targetFile.toPath(), sourceFile.toPath())
+    var buildDir = file("build")
+    if (buildDir.exists()) {
+        var targetFile = File(buildDir, "classes/java/main/org.apache.sis.referencing.epsg/META-INF/LICENSE")
+        if (!targetFile.exists()) {
+            val sourceFile = File(file("src"), "org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/LICENSE.txt")
+            if (sourceFile.exists()) {
+                var realPath = sourceFile.toPath().toRealPath()
+                Files.createLink(targetFile.toPath(), realPath)
+                targetFile = File(file("build"), "classes/java/main/org.apache.sis.referencing.database/META-INF/LICENSE")
+                Files.createLink(targetFile.toPath(), realPath)
+            }
         }
     }
 }
diff --git a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
index c53faee85b..a97598cfac 100644
--- a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
+++ b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/EmbeddedResources.java
@@ -18,6 +18,7 @@ package org.apache.sis.resources.embedded;
 
 import java.util.Set;
 import java.util.Locale;
+import java.util.StringJoiner;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.io.IOException;
@@ -30,7 +31,7 @@ import org.apache.sis.util.resources.Errors;
 
 
 /**
- * Provides an embedded database for the EPSG geodetic dataset and other resources.
+ * Provides an embedded database for the <abbr>EPSG</abbr> geodetic dataset and other resources.
  * Provides also a copy of the <a href="https://epsg.org/terms-of-use.html">EPSG terms of use</a>,
  * which should be accepted by users before the EPSG dataset can be installed.
  *
@@ -45,7 +46,7 @@ public class EmbeddedResources extends InstallationResources {
      * The name of the database embedded in the JAR file.
      * It must be an invalid package name, because otherwise the Java Platform Module System (JPMS) enforces
      * encapsulation in the same way as non-exported packages, which makes the database inaccessible to Derby.
-     * This naming trick is part of JPMS specification, so it should be reliable.
+     * This naming trick is part of <abbr>JPMS</abbr> specification, so it should be reliable.
      */
     static final String EMBEDDED_DATABASE = "spatial-metadata";
 
@@ -95,14 +96,13 @@ public class EmbeddedResources extends InstallationResources {
         } else {
             return null;
         }
-        final StringBuilder buffer = new StringBuilder();
-        final String lineSeparator = System.lineSeparator();
+        final var buffer = new StringJoiner(System.lineSeparator(), "", System.lineSeparator());
         try (BufferedReader in = new BufferedReader(new InputStreamReader(
                 EmbeddedResources.class.getResourceAsStream(filename), "UTF-8")))
         {
             String line;
             while ((line = in.readLine()) != null) {
-                buffer.append(line).append(lineSeparator);
+                buffer.add(line);
             }
         }
         return buffer.toString();
@@ -130,14 +130,14 @@ public class EmbeddedResources extends InstallationResources {
     @Override
     public DataSource getResource(String authority, int index) {
         verifyAuthority(authority);
-        final EmbeddedDataSource ds = new EmbeddedDataSource();
+        final var ds = new EmbeddedDataSource();
         ds.setDataSourceName(Initializer.DATABASE);
         ds.setDatabaseName("classpath:SIS_DATA/Databases/" + EMBEDDED_DATABASE);
         return ds;
     }
 
     /**
-     * Unconditionally throws an exception since the embedded database is not provided as SQL scripts.
+     * Unconditionally throws an exception since the embedded database is not provided as <abbr>SQL</abbr> scripts.
      *
      * @param  authority  shall be {@code "Embedded"}.
      * @param  resource   shall be 0.
diff --git a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/package-info.java b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/package-info.java
index 2ccb718065..893d4b032d 100644
--- a/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/package-info.java
+++ b/optional/src/org.apache.sis.referencing.database/main/org/apache/sis/resources/embedded/package-info.java
@@ -16,15 +16,15 @@
  */
 
 /**
- * Provides non-free data, including the EPSG geodetic dataset, in a single read-only JAR file.
- * This module contains a copy of EPSG geodetic dataset in an embedded Apache Derby database.
+ * Provides data, including the non-free <abbr>EPSG</abbr> geodetic dataset, in a single read-only <abbr>JAR</abbr> file.
+ * This module contains a copy of the <abbr>EPSG</abbr> geodetic dataset in an embedded Apache Derby database.
  * Having this module on the module-path avoid the need to set the {@code SIS_DATA} environment variable
- * for using the Coordinate Reference Systems (<abbr>CRS</abbr>) and Coordinate Operations defined by EPSG.
+ * for using the Coordinate Reference Systems (<abbr>CRS</abbr>) and Coordinate Operations defined by <abbr>EPSG</abbr>.
  *
  * <h2>Licensing</h2>
- * EPSG is maintained by the <a href="https://www.iogp.org/">International Association of Oil and Gas Producers</a>
- * (IOGP) Surveying &amp; Positioning Committee and is subject to <a href="https://epsg.org/terms-of-use.html">EPSG
- * terms of use</a>. This module is not included in the Apache <abbr>SIS</abbr> distribution of convenience binaries,
+ * <abbr>EPSG</abbr> is maintained by the <a href="https://www.iogp.org/">International Association of Oil and Gas Producers</a> (<abbr>IOGP</abbr>)
+ * Surveying &amp; Positioning Committee and is subject to <a href="https://epsg.org/terms-of-use.html"><abbr>EPSG</abbr> terms of use</a>.
+ * This module is not included in the Apache <abbr>SIS</abbr> distribution of convenience binaries,
  * and the source code contains only the Java classes without the <abbr>EPSG</abbr> data. For use in an application,
  * see <a href="https://sis.apache.org/epsg.html">How to use EPSG geodetic dataset</a> on the <abbr>SIS</abbr> web site.
  *
diff --git a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
index 6dee0b4fbb..4c8393cdcc 100644
--- a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
+++ b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java
@@ -27,10 +27,10 @@ import org.apache.sis.setup.InstallationResources;
 import org.apache.sis.metadata.sql.privy.Initializer;
 import org.apache.sis.system.DataDirectory;
 import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.factory.sql.epsg.ScriptProvider;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 import org.apache.sis.test.TestUtilities;
@@ -41,27 +41,29 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
 /**
  * Tests {@link EmbeddedResources}.
+ * This test has the side-effect of creating the database if it does not already exists.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public final strictfp class EmbeddedResourcesTest {
-    /**
-     * Whether the database has been created.
-     */
-    private static boolean databaseCreated;
-
     /**
      * Creates a new test case.
+     *
+     * @throws Exception if an error occurred while creating the database.
      */
-    public EmbeddedResourcesTest() {
+    public EmbeddedResourcesTest() throws Exception {
+        new Generator().createIfAbsent();
     }
 
     /**
-     * Skips the test if the EPSG scripts are not present.
+     * Skips the test if the <abbr>EPSG</abbr> scripts are not present.
      * This method uses {@code LICENSE.txt} as a sentinel file.
+     * Note that even if the <abbr>EPSG</abbr> data are not available,
+     * the database may still contain other metadata.
      */
-    private static void assumeDataPresent() {
-        assumeTrue(ScriptProvider.class.getResource("LICENSE.txt") != null,
+    private static void assumeContainsEPSG() {
+        assumeTrue(EmbeddedResources.class.getResource("LICENSE.txt") != null,
                 "EPSG resources not found. See `README.md` for manual installation.");
     }
 
@@ -69,13 +71,7 @@ public final strictfp class EmbeddedResourcesTest {
      * Returns the {@link EmbeddedResources} instance declared in the {@code META-INF/services/} directory.
      * The provider may coexist with providers defined in other modules, so we need to filter them.
      */
-    private static synchronized InstallationResources getInstance() {
-        if (!databaseCreated) try {
-            new Generator().run();
-            databaseCreated = true;
-        } catch (Exception e) {
-            throw new AssertionError(e);
-        }
+    private static InstallationResources getInstance() {
         InstallationResources provider = null;
         for (InstallationResources candidate : ServiceLoader.load(InstallationResources.class)) {
             if (candidate instanceof EmbeddedResources) {
@@ -94,7 +90,7 @@ public final strictfp class EmbeddedResourcesTest {
      */
     @Test
     public void testLicences() throws IOException {
-        assumeDataPresent();
+        assumeContainsEPSG();
         final InstallationResources provider = getInstance();
         assertTrue(provider.getLicense("Embedded", null, "text/plain").contains("IOGP"));
         assertTrue(provider.getLicense("Embedded", null, "text/html" ).contains("IOGP"));
@@ -107,13 +103,13 @@ public final strictfp class EmbeddedResourcesTest {
      */
     @Test
     public void testConnection() throws Exception {
-        assumeDataPresent();
+        assumeContainsEPSG();
         final String dir = DataDirectory.getenv();
-        assertTrue((dir == null) || dir.isEmpty(), "The SIS_DATA environment variable must be unset for enabling this test.");
+        assumeTrue((dir == null) || dir.isEmpty(), "The SIS_DATA environment variable must be unset for enabling this test.");
         final DataSource ds = Initializer.getDataSource();
         assertNotNull(ds, "Cannot find the data source.");
         try (Connection c = ds.getConnection()) {
-            assertEquals("jdbc:derby:classpath:SIS_DATA/Databases/spatial-metadata", c.getMetaData().getURL(), "URL");
+            assertEquals("jdbc:derby:classpath:SIS_DATA/Databases/" + EmbeddedResources.EMBEDDED_DATABASE, c.getMetaData().getURL(), "URL");
             try (Statement s = c.createStatement()) {
                 try (ResultSet r = s.executeQuery("SELECT COORD_REF_SYS_NAME FROM EPSG.\"Coordinate Reference System\" WHERE COORD_REF_SYS_CODE = 4326")) {
                     assertTrue(r.next(), "ResultSet.next()");
@@ -133,7 +129,7 @@ public final strictfp class EmbeddedResourcesTest {
      */
     @Test
     public void testCrsforCode() throws FactoryException {
-        assumeDataPresent();
+        assumeContainsEPSG();
         CoordinateReferenceSystem crs = CRS.forCode("EPSG:6676");
         String area = TestUtilities.getSingleton(crs.getDomains()).getDomainOfValidity().getDescription().toString();
         assertTrue(area.contains("Japan"), area);
diff --git a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
index fcc3b69f99..95b646aaa1 100644
--- a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
+++ b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/Generator.java
@@ -16,16 +16,11 @@
  */
 package org.apache.sis.resources.embedded;
 
-import java.util.List;
 import java.util.ArrayList;
-import java.util.Map;
 import java.util.HashMap;
-import java.io.InputStream;
 import java.io.IOException;
-import java.io.FileNotFoundException;
 import java.lang.reflect.Method;
 import java.net.URISyntaxException;
-import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.sql.CallableStatement;
@@ -41,15 +36,14 @@ import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.metadata.sql.privy.Initializer;
 import org.apache.sis.metadata.sql.privy.LocalDataSource;
 import org.apache.sis.system.DataDirectory;
-import org.apache.sis.system.Shutdown;
 import org.apache.sis.referencing.factory.sql.EPSGFactory;
 import org.apache.sis.referencing.factory.sql.epsg.ScriptProvider;
 
 
 /**
- * Generates {@code SpatialMetadata} database with EPSG geodetic dataset.
- * This class is invoked only at build time and should be excluded from the final JAR file.
- * The {@link #main(String[])} method generates resources directly in the {@code target/classes} directory.
+ * Generates {@code SpatialMetadata} database with <abbr>EPSG</abbr> geodetic dataset.
+ * This class is invoked only at build time and should be excluded from the final <abbr>JAR</abbr> file.
+ * The {@link #createIfAbsent()} method generates resources directly in the {@code target/classes} directory.
  *
  * <p><b>Note:</b>
  * Maven usage is to generate resources in the {@code target/generated-resources} directory.
@@ -58,39 +52,22 @@ import org.apache.sis.referencing.factory.sql.epsg.ScriptProvider;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class Generator extends ScriptProvider {
+final class Generator extends ScriptProvider {
     /**
-     * Generates the embedded resources in the {@code target/classes} directory.
-     * See class Javadoc for more information.
-     *
-     * @param  args  ignored. Can be null.
-     * @throws Exception if a failure occurred while searching directories,
-     *         executing SQL scripts, copying data or any other operation.
+     * Directory where the {@link EmbeddedResources} class file is located.
+     * This is the directory where to copy the license files.
      */
-    public static void main(String[] args) throws Exception {
-        new Generator().run();
-        Shutdown.stop(Generator.class);
-    }
+    private final Path classesDirectory;
 
     /**
-     * Generates the embedded resources in the {@code target/classes} directory.
-     *
-     * @throws Exception if a failure occurred while searching directories,
-     *         executing SQL scripts, copying data or any other operation.
+     * Directory where to search for <abbr>EPSG</abbr> data.
      */
-    final void run() throws Exception {
-        if (dataSource != null) {
-            createMetadata();
-            createEPSG();
-            compress();
-            shutdown();
-        }
-    }
+    private final Path sourceEPSG;
 
     /**
      * Provides a connection to the "SpatialMetadata" database, or {@code null} if the database already exists.
-     * The connection URL will reference the {@code SIS_DATA/Databases/spatial-metadata} directory in the Maven
-     * {@code target/classes} directory.
+     * The connection <abbr>URL</abbr> references the <code>SIS_DATA/Databases/{@value EmbeddedResources#EMBEDDED_DATABASE}</code>
+     * directory in the Maven {@code target/classes} directory.
      */
     private final EmbeddedDataSource dataSource;
 
@@ -98,12 +75,14 @@ public final class Generator extends ScriptProvider {
      * Creates a new database generator.
      */
     Generator() throws URISyntaxException, IOException {
-        Path target = copyLicenseFiles();
+        classesDirectory = directoryOf(EmbeddedResources.class);
+        Path target = classesDirectory;
         do target = target.getParent();     // Move to the root directory of classes.
         while (!target.getFileName().toString().startsWith("org.apache.sis."));
         target = target.resolve("META-INF").resolve(DataDirectory.ENV);
         if (Files.isDirectory(target)) {
             dataSource = null;
+            sourceEPSG = null;
             return;
         }
         /*
@@ -116,28 +95,51 @@ public final class Generator extends ScriptProvider {
         dataSource.setDataSourceName(Initializer.DATABASE);
         dataSource.setDatabaseName(target.resolve(EmbeddedResources.EMBEDDED_DATABASE).toString());
         dataSource.setCreateDatabase("create");
+        sourceEPSG = directoryOf(ScriptProvider.class);
+    }
+
+    /**
+     * Returns the directory which contains the given class.
+     */
+    private static Path directoryOf(final Class<?> member) throws URISyntaxException {
+        return Path.of(member.getResource(member.getSimpleName() + ".class").toURI()).getParent();
+    }
+
+    /**
+     * Generates the embedded resources in the {@code target/classes} directory if it does not already exists.
+     * See class Javadoc for more information.
+     *
+     * @throws Exception if a failure occurred while searching directories,
+     *         executing <abbr>SQL</abbr> scripts, copying data or any other operation.
+     */
+    final void createIfAbsent() throws Exception {
+        if (dataSource != null) {
+            copyLicenseFiles();
+            createMetadata();
+            createEPSG();
+            compress();
+            shutdown();
+        }
     }
 
     /**
-     * Copies the EPSG terms of use from the {@code sis-epsg} module to this {@code sis-embedded-data} module.
-     * We copy those files ourselves instead than relying on {@code maven-resources-plugin} because a future
-     * version may combine more licenses in a single file.
+     * Copies the <abbr>EPSG</abbr> terms of use from the {@code sis-epsg} module to this {@code sis-embedded-data} module.
+     * If the <abbr>EPSG</abbr> data are not found, then this method does nothing.
      *
-     * @return the directory where the licenses have been copied.
+     * <p>We copy those files ourselves instead than relying on {@code maven-resources-plugin}
+     * because a future version may combine more licenses in a single file.</p>
      */
-    private Path copyLicenseFiles() throws URISyntaxException, IOException {
-        final Class<?> consumer = EmbeddedResources.class;
-        final Path target = Path.of(consumer.getResource(consumer.getSimpleName() + ".class").toURI()).getParent();
+    private void copyLicenseFiles() throws URISyntaxException, IOException {
         final String[] files = {"LICENSE.txt", "LICENSE.html"};
         for (String file : files) {
-            try (InputStream in = openStream(file)) {
-                if (in == null) throw new FileNotFoundException(file);
-                Files.copy(in, target.resolve(file));
-            } catch (FileAlreadyExistsException e) {
-                break;
+            final Path source = sourceEPSG.resolve(file);
+            if (Files.exists(source)) {
+                final Path target = classesDirectory.resolve(file);
+                if (Files.notExists(target)) {
+                    Files.createLink(target, source);
+                }
             }
         }
-        return target;
     }
 
     /**
@@ -146,7 +148,7 @@ public final class Generator extends ScriptProvider {
      * @throws FactoryException  if an error occurred while creating or querying the database.
      */
     private void createMetadata() throws MetadataStoreException, ReflectiveOperationException {
-        try (MetadataSource md = new MetadataSource(MetadataStandard.ISO_19115, dataSource, "metadata", null)) {
+        try (var md = new MetadataSource(MetadataStandard.ISO_19115, dataSource, "metadata", null)) {
             Method install = md.getClass().getDeclaredMethod("install");
             install.setAccessible(true);
             install.invoke(md);
@@ -154,12 +156,18 @@ public final class Generator extends ScriptProvider {
     }
 
     /**
-     * Creates the EPSG database schema.
+     * Creates the <abbr>EPSG</abbr> database schema.
+     * This method does nothing if the <abbr>EPSG</abbr> are not available.
+     * In the latter case, only the database will contain only free metadata.
      *
      * @throws FactoryException  if an error occurred while creating or querying the database.
+     * @return whether the <abbr>EPSG</abbr> data were found.
      */
-    private void createEPSG() throws FactoryException {
-        final Map<String,Object> properties = new HashMap<>();
+    private boolean createEPSG() throws FactoryException {
+        if (Files.notExists(sourceEPSG.resolve("Data.sql"))) {
+            return false;
+        }
+        final var properties = new HashMap<String,Object>();
         properties.put("dataSource", dataSource);
         properties.put("scriptProvider", this);
         /*
@@ -173,16 +181,17 @@ public final class Generator extends ScriptProvider {
         if (!crs.getName().getCode().equals("WGS 84")) {
             throw new FactoryException("Unexpected CRS: " + crs.getName());
         }
+        return true;
     }
 
     /**
-     * Compresses all tables in the EPSG schema. Compression can save space if there was many update
+     * Compresses all tables in all schema. Compression can save space if there was many update
      * or delete operations in the database. In the case of the database generated by this class,
      * the benefit is very small because the database is fresh. But it is still non-zero.
      */
     private void compress() throws SQLException {
         try (Connection c = dataSource.getConnection()) {
-            List<String> tables = new ArrayList<>(80);  // As (schema,table) pairs.
+            final var tables = new ArrayList<String>(80);  // As (schema,table) pairs.
             try (ResultSet r = c.getMetaData().getTables(null, null, null, null)) {
                 while (r.next()) {
                     final String schema = r.getString("TABLE_SCHEM");
diff --git a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/README.md b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/README.md
index bfd7a72114..115306fa6b 100644
--- a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/README.md
+++ b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/README.md
@@ -9,16 +9,13 @@ the following commands must be executed manually in a separated directory:
 ```shell
 svn checkout https://svn.apache.org/repos/asf/sis/data/non-free/
 cd non-free/EPSG
-export NON_FREE_DIR=$PWD
+export EPSG_DIR=$PWD
 ```
 
-Then, the following commands must be executed with this directory as the current directory:
+Then, the following commands (or something equivalent) should be executed
+with the directory of this `README.md` file as the current directory:
 
 ```shell
-ln --symbolic $NON_FREE_DIR/LICENSE.txt
-ln --symbolic $NON_FREE_DIR/LICENSE.html
-ln --symbolic $NON_FREE_DIR/Tables.sql
-ln --symbolic $NON_FREE_DIR/Data.sql
-ln --symbolic $NON_FREE_DIR/FKeys.sql
-cd -
+ln --symbolic $EPSG_DIR/LICENSE.* .
+ln --symbolic $EPSG_DIR/*.sql .
 ```
diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/ScriptProviderTest.java b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/ScriptProviderTest.java
index f18c0c8b77..71792815a2 100644
--- a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/ScriptProviderTest.java
+++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/ScriptProviderTest.java
@@ -80,11 +80,11 @@ public final strictfp class ScriptProviderTest {
     public void testResources() throws IOException {
         final InstallationResources provider = getInstance();
         final String[] names = provider.getResourceNames("EPSG");
-        assertArrayEquals(new String[] {"Prepare", "Tables.sql", "Data.sql", "FKeys.sql", "Finish"}, names);
+        assertArrayEquals(new String[] {"Prepare.sql", "Tables.sql", "Data.sql", "FKeys.sql", "Finish.sql"}, names);
         for (int i=0; i<names.length; i++) {
             try (BufferedReader in = provider.openScript("EPSG", i)) {
                 // Just verify that we can read.
-                assertFalse(in.readLine().isEmpty());
+                assertFalse(in.readLine().isBlank());
             }
         }
     }


(sis) 02/02: Add "World Mercator" and "Pseudo-Mercator" in the set of CRS that are always present. There is a high demand for them, and they are needed for passing some tests anyway.

Posted by de...@apache.org.
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 b0f406a181ce72e5b3a04f558f39f12afa865796
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Aug 20 19:45:32 2025 +0200

    Add "World Mercator" and "Pseudo-Mercator" in the set of CRS that are always present.
    There is a high demand for them, and they are needed for passing some tests anyway.
---
 .../apache/sis/referencing/AuthorityFactories.java | 29 +++++----
 .../main/org/apache/sis/referencing/CRS.java       |  2 +
 .../main/org/apache/sis/referencing/CommonCRS.java | 14 ++---
 .../sis/referencing/EPSGFactoryFallback.java       | 41 +++++++++----
 .../sis/referencing/StandardDefinitions.java       | 70 ++++++++++++++++++----
 .../operation/provider/Mercator1SP.java            |  7 ++-
 .../operation/provider/PolarStereographicA.java    |  2 +-
 .../operation/provider/PseudoMercator.java         |  7 ++-
 .../operation/provider/TransverseMercator.java     |  2 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  4 +-
 .../sis/referencing/EPSGFactoryFallbackTest.java   |  2 +-
 .../sis/referencing/StandardDefinitionsTest.java   | 29 +++++++++
 12 files changed, 156 insertions(+), 53 deletions(-)

diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
index 6cebdd68b9..711f3dde50 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java
@@ -46,10 +46,10 @@ import org.apache.sis.util.logging.Logging;
 
 
 /**
- * Provides the CRS, CS, datum and coordinate operation authority factories.
+ * Provides the <abbr>CRS</abbr>, <abbr>CS</abbr>, datum and coordinate operation authority factories.
  * Provides also the system-wide {@link MultiAuthoritiesFactory} instance used by {@link CRS#forCode(String)}.
- * Current version handles the EPSG factory in a special way, but we may try to avoid doing special cases in a
- * future SIS version (this may require more help from {@link ServiceLoader}).
+ * Current version handles the <abbr>EPSG</abbr> factory in a special way, but we may try to avoid doing special
+ * cases in a future <abbr>SIS</abbr> version (this may require more help from {@link ServiceLoader}).
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -134,7 +134,7 @@ final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> {
     @Override
     @SuppressWarnings("unchecked")
     protected T[] initialValues() {
-        return (T[]) new GeodeticAuthorityFactory[] {getEPSG()};
+        return (T[]) new GeodeticAuthorityFactory[] {getEPSG(true)};
     }
 
     /**
@@ -162,16 +162,19 @@ final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> {
     }
 
     /**
-     * Returns the factory connected to the EPSG geodetic dataset if possible, or the EPSG fallback otherwise.
-     * If an EPSG data source has been found, then this method returns an instance of {@link EPSGFactory}, but
-     * there is no guarantee that attempts to use that factory will succeed. For example, maybe the EPSG schema
-     * does not exist. Callers should be prepared to either receive an {@link EPSGFactoryFallback} directly if
-     * the EPSG data source does not exist, or replace the {@code EPSGFactory} by a {@code EPSGFactoryFallback}
-     * later if attempt to use the returned factory fails.
+     * Returns the factory connected to the <abbr>EPSG</abbr> geodetic dataset if possible, or the fallback otherwise.
+     * If an <abbr>EPSG</abbr> data source has been found, then this method returns an instance of {@link EPSGFactory}.
+     * But unless {@code test} is {@code true}, there is no guarantee that attempts to use that factory will succeed.
+     * For example, maybe the {@code EPSG} schema does not exist and no installation scripts are available on the module-path.
+     * Callers should be prepared to either receive an {@link EPSGFactoryFallback} directly if the EPSG data source does not exist,
+     * or replace the {@code EPSGFactory} by a {@code EPSGFactoryFallback} later if attempt to use the returned factory fails.
      */
-    static synchronized GeodeticAuthorityFactory getEPSG() {
+    static synchronized GeodeticAuthorityFactory getEPSG(final boolean test) {
         if (EPSG == null) try {
             EPSG = new EPSGFactory(null);
+            if (test) {
+                EPSG.createPrimeMeridian(StandardDefinitions.GREENWICH);
+            }
         } catch (FactoryException e) {
             log(e, false);
             EPSG = EPSGFactoryFallback.INSTANCE;
@@ -231,7 +234,7 @@ final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> {
 
     /**
      * Logs the given exception at the given level. This method pretends that the logging come from
-     * {@link CRS#getAuthorityFactory(String)}, which is the public facade for {@link #getEPSG()}.
+     * {@link CRS#getAuthorityFactory(String)}, which is the public facade for {@link #getEPSG(boolean)}.
      */
     private static void log(final Exception e, final boolean isWarning) {
         String message = e.getMessage();        // Prefer the locale of system administrator.
@@ -253,7 +256,7 @@ final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> {
      * @throws FactoryException if the finder cannot be created.
      */
     static IdentifiedObjectFinder finderForEPSG() throws FactoryException {
-        final GeodeticAuthorityFactory factory = getEPSG();
+        final GeodeticAuthorityFactory factory = getEPSG(false);
         if (factory instanceof EPSGFactoryFallback) {
             return ((EPSGFactoryFallback) factory).newIdentifiedObjectFinder();
         }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index aa52105478..d098c91904 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -179,6 +179,8 @@ public final class CRS extends Static {
      *   <tr><td>CRS:83</td>        <td>{@link CommonCRS#NAD83  NAD83}</td>  <td>Geographic</td>      <td>Like EPSG:4269 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr>
      *   <tr><td>CRS:84</td>        <td>{@link CommonCRS#WGS84  WGS84}</td>  <td>Geographic</td>      <td>Like EPSG:4326 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr>
      *   <tr><td>CRS:88</td>        <td>{@link CommonCRS.Vertical#NAVD88 NAVD88}</td><td>Vertical</td><td>North American Vertical Datum 1988 height</td></tr>
+     *   <tr><td>EPSG:3395</td>     <td>{@link CommonCRS#WGS84  WGS84}</td>  <td>Projected</td>       <td>WGS 84 / World Mercator</td></tr>
+     *   <tr><td>EPSG:3857</td>     <td>{@link CommonCRS#WGS84  WGS84}</td>  <td>Projected</td>       <td>WGS 84 / Pseudo-Mercator</td></tr>
      *   <tr><td>EPSG:4230</td>     <td>{@link CommonCRS#ED50   ED50}</td>   <td>Geographic</td>      <td>European Datum 1950</td></tr>
      *   <tr><td>EPSG:4258</td>     <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geographic</td>      <td>European Terrestrial Reference System 1989</td></tr>
      *   <tr><td>EPSG:4267</td>     <td>{@link CommonCRS#NAD27  NAD27}</td>  <td>Geographic</td>      <td>North American Datum 1927</td></tr>
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
index 404beba383..003bce6e6e 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CommonCRS.java
@@ -1149,9 +1149,10 @@ public enum CommonCRS {
             if (cs == null) {
                 if (this != DEFAULT) {
                     cs = DEFAULT.universal(latitude, longitude).getCoordinateSystem();
+                } else if (isUTM) {
+                    cs = StandardDefinitions.defaultCartesianCS();
                 } else {
                     cs = (CartesianCS) StandardDefinitions.createCoordinateSystem(
-                            isUTM   ? StandardDefinitions.CARTESIAN_2D :
                             isSouth ? StandardDefinitions.UPS_SOUTH
                                     : StandardDefinitions.UPS_NORTH, true);
                 }
@@ -2055,17 +2056,12 @@ public enum CommonCRS {
     }
 
     /**
-     * Returns the EPSG factory to use for creating CRS, or {@code null} if none.
+     * Returns the <abbr>EPSG</abbr> factory to use for creating <abbr>CRS</abbr>s, or {@code null} if none.
      * If this method returns {@code null}, then the caller will silently fallback on hard-coded values.
      */
     private static GeodeticAuthorityFactory factory() {
-        if (!EPSGFactoryFallback.FORCE_HARDCODED) {
-            final GeodeticAuthorityFactory factory = AuthorityFactories.getEPSG();
-            if (!(factory instanceof EPSGFactoryFallback)) {
-                return factory;
-            }
-        }
-        return null;
+        final GeodeticAuthorityFactory factory = AuthorityFactories.getEPSG(false);
+        return (factory instanceof EPSGFactoryFallback) ? null : factory;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EPSGFactoryFallback.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EPSGFactoryFallback.java
index 10eac1745b..9fc223597c 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EPSGFactoryFallback.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/EPSGFactoryFallback.java
@@ -45,7 +45,6 @@ import org.apache.sis.referencing.operation.provider.TransverseMercator;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.system.Fallback;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.Debug;
 import org.apache.sis.util.privy.MetadataServices;
 import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.URLs;
@@ -70,14 +69,6 @@ import org.opengis.referencing.crs.GeodeticCRS;
 final class EPSGFactoryFallback extends GeodeticAuthorityFactory
         implements CRSAuthorityFactory, CSAuthorityFactory, DatumAuthorityFactory
 {
-    /**
-     * Whether to disallow {@code CommonCRS} to use {@link org.apache.sis.referencing.factory.sql.EPSGFactory}
-     * (in which case {@code CommonCRS} will fallback on hard-coded values).
-     * This field should always be {@code false}, except for debugging purposes.
-     */
-    @Debug
-    static final boolean FORCE_HARDCODED = false;
-
     /**
      * The singleton instance.
      */
@@ -92,10 +83,16 @@ final class EPSGFactoryFallback extends GeodeticAuthorityFactory
      */
     private String installationURL;
 
+    /**
+     * Cache of <abbr>CRS</abbr>s other than the one cached by {@link CommonCRS}.
+     */
+    private final ProjectedCRS[] cache;
+
     /**
      * Constructor for the singleton instance.
      */
     private EPSGFactoryFallback() {
+        cache = new ProjectedCRS[2];
     }
 
     /**
@@ -135,7 +132,7 @@ final class EPSGFactoryFallback extends GeodeticAuthorityFactory
         final boolean geographic = type.isAssignableFrom(GeographicCRS.class);
         final boolean geodetic   = type.isAssignableFrom(GeodeticCRS  .class);
         final boolean projected  = type.isAssignableFrom(ProjectedCRS .class);
-        final Set<String> codes = new LinkedHashSet<>();
+        final var codes = new LinkedHashSet<String>();
         if (pm) codes.add(StandardDefinitions.GREENWICH);
         for (final CommonCRS crs : CommonCRS.values()) {
             if (ellipsoid) add(codes, crs.ellipsoid);
@@ -291,6 +288,30 @@ final class EPSGFactoryFallback extends GeodeticAuthorityFactory
             final int s = Math.max(code.lastIndexOf(Constants.DEFAULT_SEPARATOR), code.lastIndexOf('#'));
             code = CharSequences.trimWhitespaces(code, s + 1, code.length()).toString();
             final short n = Short.parseShort(code);
+            /*
+             * Special case for CRSs which do not have a convenience method in `CommonCRS`.
+             * The absence of convenience method is because those projections may not be reasonable with all datums.
+             * For example, Pseudo-Mercator is used with WGS 84 only. While it would be technically possible to apply
+             * it on any datum, this is not something that we want to encourage.
+             */
+            if ((kind & CRS) != 0) {
+                final boolean pseudo = (n == 3857);     // Pseudo-Mercator
+                if (pseudo || n == 3395) {              // or World Mercator
+                    final int index = pseudo ? 1 : 0;
+                    ProjectedCRS crs;
+                    synchronized (cache) {
+                        crs = cache[index];
+                        if (crs == null) {
+                            crs = StandardDefinitions.createMercator(n, CommonCRS.WGS84.geographic(), pseudo);
+                            cache[index] = crs;
+                        }
+                    }
+                    return crs;
+                }
+            }
+            /*
+             * Cases that we can delegate to `CommonCRS`. That enumeration have its own cache.
+             */
             if ((kind & (ELLIPSOID | DATUM | CRS)) != 0) {
                 for (final CommonCRS crs : CommonCRS.values()) {
                     /*
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 ac942698a4..92fc9c85b7 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
@@ -18,6 +18,7 @@ package org.apache.sis.referencing;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.function.Function;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
 import org.opengis.util.InternationalString;
@@ -48,6 +49,8 @@ import org.apache.sis.util.privy.Constants;
 import org.apache.sis.metadata.privy.AxisNames;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.operation.DefaultConversion;
+import org.apache.sis.referencing.operation.provider.Mercator1SP;
+import org.apache.sis.referencing.operation.provider.PseudoMercator;
 import org.apache.sis.referencing.operation.provider.TransverseMercator;
 import org.apache.sis.referencing.operation.provider.PolarStereographicA;
 import org.apache.sis.util.resources.Vocabulary;
@@ -93,10 +96,10 @@ final class StandardDefinitions {
      * We refer to latest version of the 9.x series instead of more recent
      * data because the fallback CRS does not use datum ensemble.
      */
-    static final String VERSION = "9.9.1";
+    private static final String VERSION = "9.9.1";
 
     /**
-     * The EPSG code for Greenwich meridian.
+     * The <abbr>EPSG</abbr> code for Greenwich meridian.
      *
      * @see org.apache.sis.util.privy.Constants#EPSG_GREENWICH
      */
@@ -170,11 +173,20 @@ final class StandardDefinitions {
         });
     }
 
+    /**
+     * Creates a Mercator projection using the Apache <abbr>SIS</abbr> factory implementation.
+     *
+     * @param  pseudo  whether to create the pseudo-Mercator projection.
+     */
+    static ProjectedCRS createMercator(final int code, final GeographicCRS baseCRS, final boolean pseudo) {
+        return createProjectedCRS(code, baseCRS, defaultCartesianCS(), pseudo ? PseudoMercator.NAME : Mercator1SP.NAME, (parameters) -> {
+            return pseudo ? "Pseudo-Mercator" : "World Mercator";
+        });
+    }
+
     /**
      * Creates a Universal Transverse Mercator (UTM) or a Universal Polar Stereographic (UPS) projected CRS
-     * using the Apache SIS factory implementation. This method restricts the factory to SIS implementation
-     * instead of arbitrary factory in order to met the contract saying that {@link CommonCRS} methods
-     * should never fail.
+     * using the Apache <abbr>SIS</abbr> factory implementation.
      *
      * @param code       the EPSG code, or 0 if none.
      * @param baseCRS    the geographic CRS on which the projected CRS is based.
@@ -185,19 +197,36 @@ final class StandardDefinitions {
      */
     static ProjectedCRS createUniversal(final int code, final GeographicCRS baseCRS, final boolean isUTM,
             final double latitude, final double longitude, final CartesianCS derivedCS)
+    {
+        return createProjectedCRS(code, baseCRS, derivedCS, isUTM ? TransverseMercator.NAME : PolarStereographicA.NAME, (parameters) -> {
+            return isUTM ? TransverseMercator.Zoner.UTM.setParameters(parameters, latitude, longitude)
+                         : PolarStereographicA.setParameters(parameters, latitude >= 0);
+        });
+    }
+
+    /**
+     * Creates a projected CRS from hard-coded values for the given code.
+     * This method restricts the factory to the <abbr>SIS</abbr> implementation instead of arbitrary factory
+     * in order to met the contract saying that {@link CommonCRS} methods should never fail.
+     *
+     * @param code        the EPSG code, or 0 if none.
+     * @param baseCRS     the geographic CRS on which the projected CRS is based.
+     * @param derivedCS   the projected coordinate system.
+     * @param methodName  name of the operation method to apply.
+     * @param setup       a function which setup the parameter values and returns the name of the conversion.
+     */
+    private static ProjectedCRS createProjectedCRS(final int code, final GeographicCRS baseCRS, final CartesianCS derivedCS,
+            final String methodName, final Function<ParameterValueGroup, String> setup)
     {
         final OperationMethod method;
         try {
-            method = DefaultMathTransformFactory.provider()
-                     .getOperationMethod(isUTM ? TransverseMercator.NAME : PolarStereographicA.NAME);
+            method = DefaultMathTransformFactory.provider().getOperationMethod(methodName);
         } catch (NoSuchIdentifierException e) {
             throw new IllegalStateException(e);                     // Should not happen with SIS implementation.
         }
         final ParameterValueGroup parameters = method.getParameters().createValue();
-        String name = isUTM ? TransverseMercator.Zoner.UTM.setParameters(parameters, latitude, longitude)
-                            : PolarStereographicA.setParameters(parameters, latitude >= 0);
+        String name = setup.apply(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);
     }
@@ -363,6 +392,21 @@ final class StandardDefinitions {
                        UPS_NORTH      = (short) 1026,
                        UPS_SOUTH      = (short) 1027;
 
+    /**
+     * The default coordinate system for projected <abbr>CRS</abbr>, created when first requested.
+     */
+    private static CartesianCS DEFAULT_CS;
+
+    /**
+     * Returns the default coordinate system for projected <abbr>CRS</abbr>.
+     */
+    static synchronized CartesianCS defaultCartesianCS() {
+        if (DEFAULT_CS == null) {
+            DEFAULT_CS = (CartesianCS) createCoordinateSystem(CARTESIAN_2D, true);
+        }
+        return DEFAULT_CS;
+    }
+
     /**
      * Creates a coordinate system from hard-coded values for the given code.
      * The coordinate system names used by this method contains only the first
@@ -383,9 +427,9 @@ final class StandardDefinitions {
             case ELLIPSOIDAL_3D: name = "Ellipsoidal 3D";  type = 2; dim = 3; axisCode =  111; break;
             case SPHERICAL:      name = "Spherical";       type = 1; dim = 3; axisCode =   63; break;
             case EARTH_CENTRED:  name = "Cartesian 3D (geocentric)"; dim = 3; axisCode =  118; break;
-            case CARTESIAN_2D:   name = "Cartesian 2D";                      axisCode =    3; break;
-            case UPS_NORTH:      name = "Cartesian 2D for UPS north";        axisCode = 1067; break;
-            case UPS_SOUTH:      name = "Cartesian 2D for UPS south";        axisCode = 1059; break;
+            case CARTESIAN_2D:   name = "Cartesian 2D";                       axisCode =    3; break;
+            case UPS_NORTH:      name = "Cartesian 2D for UPS north";         axisCode = 1067; break;
+            case UPS_SOUTH:      name = "Cartesian 2D for UPS south";         axisCode = 1059; break;
             default:   if (!mandatory) return null;
                        throw new AssertionError(code);
         }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Mercator1SP.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Mercator1SP.java
index 0f8ffa58e0..b46d138e33 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Mercator1SP.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Mercator1SP.java
@@ -45,6 +45,11 @@ public final class Mercator1SP extends AbstractMercator {
      */
     public static final String IDENTIFIER = "9804";
 
+    /**
+     * The {@value} string, which is the <abbr>EPSG</abbr> name for this projection.
+     */
+    public static final String NAME = "Mercator (variant A)";
+
     /**
      * The operation parameter descriptor for the <cite>Latitude of natural origin</cite> (φ₀) parameter value.
      * In theory, this parameter should not be used and its value should be 0 in all cases.
@@ -128,7 +133,7 @@ public final class Mercator1SP extends AbstractMercator {
 
         PARAMETERS = builder
                 .addIdentifier(              IDENTIFIER)                    // The ellipsoidal case
-                .addName(                    "Mercator (variant A)")        // Starting from EPSG version 7.6
+                .addName(                    NAME)                          // Starting from EPSG version 7.6
                 .addName(                    "Mercator (1SP)")              // Prior to EPSG version 7.6
                 .addName(Citations.OGC,      "Mercator_1SP")
                 .addName(Citations.GEOTIFF,  "CT_Mercator")
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PolarStereographicA.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PolarStereographicA.java
index 5676b80cfd..e2de3ea7fa 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PolarStereographicA.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/PolarStereographicA.java
@@ -47,7 +47,7 @@ public final class PolarStereographicA extends AbstractStereographic {
     private static final long serialVersionUID = 538262714055500925L;
 
     /**
-     * The EPSG name for this projection.
+     * The {@value} string, which is the <abbr>EPSG</abbr> name for this projection.
      */
     public static final String NAME = "Polar Stereographic (variant A)";
 
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 c1d377690e..e705d6a588 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
@@ -39,6 +39,11 @@ public final class PseudoMercator extends AbstractMercator {
      */
     public static final String IDENTIFIER = "1024";
 
+    /**
+     * The {@value} string, which is the <abbr>EPSG</abbr> name for this projection.
+     */
+    public static final String NAME = "Popular Visualisation Pseudo Mercator";
+
     /**
      * The group of all parameters expected by this coordinate operation.
      */
@@ -46,7 +51,7 @@ public final class PseudoMercator extends AbstractMercator {
     static {
         PARAMETERS = builder()
                 .addIdentifier(IDENTIFIER)
-                .addName("Popular Visualisation Pseudo Mercator")
+                .addName(NAME)
                 .createGroupForMapProjection(toArray(MercatorSpherical.PARAMETERS.descriptors(), 0));
     }
 
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/TransverseMercator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/TransverseMercator.java
index 1874bc60d6..749c22e34e 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/TransverseMercator.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/TransverseMercator.java
@@ -48,7 +48,7 @@ public final class TransverseMercator extends AbstractMercator {
     private static final long serialVersionUID = -3386587506686432398L;
 
     /**
-     * The {@value} string, which is also the EPSG name for this projection.
+     * The {@value} string, which is the <abbr>EPSG</abbr> name for this projection.
      */
     public static final String NAME = "Transverse Mercator";
 
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
index 72fc601c50..c03e0ad1c1 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
@@ -81,9 +81,7 @@ public final class CRSTest extends TestCaseWithLogs {
     private static void verifyForCode(final SingleCRS expected, final String code) throws FactoryException {
         final CoordinateReferenceSystem actual = CRS.forCode(code);
         assertTrue(Utilities.deepEquals(expected, actual, ComparisonMode.DEBUG), code);
-        if (!EPSGFactoryFallback.FORCE_HARDCODED) {
-            assertSame(expected, actual, code);
-        }
+        assertSame(expected, actual, code);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/EPSGFactoryFallbackTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/EPSGFactoryFallbackTest.java
index 69bac5ec7d..b22c338826 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/EPSGFactoryFallbackTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/EPSGFactoryFallbackTest.java
@@ -256,7 +256,7 @@ public final class EPSGFactoryFallbackTest extends TestCaseWithLogs {
      */
     @Test
     public void compareAllCodes() throws FactoryException {
-        final GeodeticAuthorityFactory EPSG = AuthorityFactories.getEPSG();
+        final GeodeticAuthorityFactory EPSG = AuthorityFactories.getEPSG(false);
         try {
             setEPSGFactory(EPSGFactoryFallback.INSTANCE);
             final var codes = new ArrayList<String>(EPSGFactoryFallback.INSTANCE.getAuthorityCodes(CoordinateReferenceSystem.class));
diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/StandardDefinitionsTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/StandardDefinitionsTest.java
index 2c15a96290..1e77145a0c 100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/StandardDefinitionsTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/StandardDefinitionsTest.java
@@ -79,6 +79,35 @@ public final class StandardDefinitionsTest extends EPSGDependentTestCase {
         assertEquals(AxisDirection.NORTH, cs.getAxis(1).getDirection());
     }
 
+    /**
+     * Tests {@link StandardDefinitions#createMercator(int, GeographicCRS, boolean)} for World Mercator.
+     */
+    @Test
+    public void testCreateWorldMercator() {
+        final ProjectedCRS crs = StandardDefinitions.createMercator(3395, HardCodedCRS.WGS84, false);
+        assertEquals("WGS 84 / World Mercator", crs.getName().getCode());
+        final ParameterValueGroup pg = crs.getConversionFromBase().getParameterValues();
+        assertEquals(0, pg.parameter(Constants.LATITUDE_OF_ORIGIN).doubleValue(), Constants.LATITUDE_OF_ORIGIN);
+        assertEquals(0, pg.parameter(Constants.CENTRAL_MERIDIAN)  .doubleValue(), Constants.CENTRAL_MERIDIAN);
+        assertEquals(1, pg.parameter(Constants.SCALE_FACTOR)      .doubleValue(), Constants.SCALE_FACTOR);
+        assertEquals(0, pg.parameter(Constants.FALSE_EASTING)     .doubleValue(), Constants.FALSE_EASTING);
+        assertEquals(0, pg.parameter(Constants.FALSE_NORTHING)    .doubleValue(), Constants.FALSE_NORTHING);
+    }
+
+    /**
+     * Tests {@link StandardDefinitions#createMercator(int, GeographicCRS, boolean)} for pseudo-Mercator.
+     */
+    @Test
+    public void testCreatePseudoMercator() {
+        final ProjectedCRS crs = StandardDefinitions.createMercator(3857, HardCodedCRS.WGS84, true);
+        assertEquals("WGS 84 / Pseudo-Mercator", crs.getName().getCode());
+        final ParameterValueGroup pg = crs.getConversionFromBase().getParameterValues();
+        assertEquals(0, pg.parameter(Constants.LATITUDE_OF_ORIGIN).doubleValue(), Constants.LATITUDE_OF_ORIGIN);
+        assertEquals(0, pg.parameter(Constants.CENTRAL_MERIDIAN)  .doubleValue(), Constants.CENTRAL_MERIDIAN);
+        assertEquals(0, pg.parameter(Constants.FALSE_EASTING)     .doubleValue(), Constants.FALSE_EASTING);
+        assertEquals(0, pg.parameter(Constants.FALSE_NORTHING)    .doubleValue(), Constants.FALSE_NORTHING);
+    }
+
     /**
      * Tests {@link StandardDefinitions#createUniversal(int, GeographicCRS, boolean, double, double, CartesianCS)}
      * for a Universal Transverse Mercator (UTM) projection.