From 2d8d935a50f77200b35bdf1bb42d8fd54a42a957 Mon Sep 17 00:00:00 2001 From: jampukka Date: Fri, 23 Oct 2026 14:33:07 +0300 Subject: [PATCH 1/3] Get the GeoPackage CRS definition from the projection factory --- CHANGELOG.md | 4 ++++ docs/overview/modules.md | 4 ++-- .../jhe/JHeProjectionTransformerFactory.java | 5 +++++ .../hakunapi-tiles-generator/README.md | 3 ++- .../hakunapi-tiles-generator/pom.xml | 6 ++++++ .../tiles/generator/TilesGenerator.java | 8 +++++++- .../tiles/generator/PyramidBoundsTest.java | 3 ++- .../tiles/generator/PyramidGeneratorTest.java | 7 ++++--- .../ProjectionTransformerFactory.java | 4 ++++ .../core/param/BboxAntimeridianTest.java | 5 +++++ .../hakunapi/core/param/BboxCrsParamTest.java | 5 +++++ .../simple/HakunaPropertyGeometryTest.java | 5 +++++ src/hakunapi-gpkg/pom.xml | 5 ----- .../gpkg/GPKGFeatureCollectionWriter.java | 2 +- .../hakunapi/gpkg/GPKGSpatialRefSystems.java | 19 ++++++++----------- .../GeoToolsProjectionTransformerFactory.java | 5 +++++ .../gt/GeoToolsProjectionTransformerTest.java | 9 +++++++++ 17 files changed, 74 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d5f1a0..22c29da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking changes (migration guide) + +- `ProjectionTransformerFactory` has a new required method `String getWKT(int srid)`, the WKT 1 definition of a CRS the factory supports. Custom implementations must add it. `hakunapi-gpkg` uses it for `gpkg_spatial_ref_sys` and no longer depends on GeoTools (`gt-epsg-hsql`) itself; the GeoPackage output now needs a projection factory that provides WKT, as `hakunapi-proj-gt` does. `JHeProjectionTransformerFactory` (community) throws `UnsupportedOperationException` for now. + ### Internationalization / Localization - **Language negotiation for descriptive elements.** The landing page, diff --git a/docs/overview/modules.md b/docs/overview/modules.md index b51d8d9b..2f07d30d 100644 --- a/docs/overview/modules.md +++ b/docs/overview/modules.md @@ -295,7 +295,7 @@ Key differences between PostGIS and GeoPackage support in Hakunapi: [`hakunapi-tiles-source-pmtiles`](../../src-community/hakunapi-tiles-source-pmtiles/README.md) *(community module)* is a `MapTileSource` and a `VectorTileSource` serving the tiles of a PMTiles v3 archive, read in place from a local file or over HTTP range requests. It is found with `ServiceLoader`, so it is enabled by being on the classpath, and is not bundled in the reference webapp. It depends on `hakunapi-tiles-core` and `slf4j-api`; it adds no new third-party dependency. -[`hakunapi-tiles-generator`](../../src-community/hakunapi-tiles-generator/README.md) *(community module)* is a command-line seed tool, not a server module: `TilesGenerator` writes a vector tile pyramid into a GeoPackage from the feature sources of a service configuration, and `PMTilesConvert` turns such a pyramid into a PMTiles v3 archive. Run `TilesGenerator` with its jar (`java -cp fi.nls.hakunapi.tiles.generator.TilesGenerator `), and `PMTilesConvert` the same way with its main class and ` `. It depends at compile time on `hakunapi-tiles-core`, `hakunapi-tiles-source-vectortile`, `hakunapi-gpkg`, `hakunapi-cql2` and `hakunapi-tiles-source-pmtiles`, and at runtime on `hakunapi-crs-properties`, which is loaded with `ServiceLoader` and not imported. Its only test dependency is `junit`. It is not bundled in the reference webapp. +[`hakunapi-tiles-generator`](../../src-community/hakunapi-tiles-generator/README.md) *(community module)* is a command-line seed tool, not a server module: `TilesGenerator` writes a vector tile pyramid into a GeoPackage from the feature sources of a service configuration, and `PMTilesConvert` turns such a pyramid into a PMTiles v3 archive. Run `TilesGenerator` with its jar (`java -cp fi.nls.hakunapi.tiles.generator.TilesGenerator `), and `PMTilesConvert` the same way with its main class and `
`. It depends at compile time on `hakunapi-tiles-core`, `hakunapi-tiles-source-vectortile`, `hakunapi-gpkg`, `hakunapi-cql2` and `hakunapi-tiles-source-pmtiles`, and at runtime on `hakunapi-crs-properties`, which is loaded with `ServiceLoader` and not imported, and `hakunapi-proj-gt`, the default projection factory, loaded by class name. Its only test dependency is `junit`. It is not bundled in the reference webapp. ## Output data formats (stable) @@ -323,7 +323,7 @@ The map on the items and item pages is a demo feature. A background tile layer i ### GeoPackage -The [GeoPackage (GPKG)](https://www.geopackage.org/) output format (the media type `application/geopackage+vnd.sqlite3`) is supported by the `hakunapi-gpgk` module. When needed it must be enabled using a configuration like `formats=geojson,gpgk`. The implementation supports outputting same feature, feature collection, property and geometry objects (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and GeometryCollection) as GeoJSON writer too. +The [GeoPackage (GPKG)](https://www.geopackage.org/) output format (the media type `application/geopackage+vnd.sqlite3`) is supported by the `hakunapi-gpgk` module. When needed it must be enabled using a configuration like `formats=geojson,gpgk`. The implementation supports outputting same feature, feature collection, property and geometry objects (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and GeometryCollection) as GeoJSON writer too. The CRS definition (WKT) written to `gpkg_spatial_ref_sys` comes from the collection's projection factory (`ProjectionTransformerFactory.getWKT`), so the module itself has no GeoTools dependency. Currently [OGC API Features](https://ogcapi.ogc.org/features/) does not specify anything related to GPGK. Some feature service metadata (like next links) cannot be written in content payload like in the case of the default GeoJSON output. Such metadata should be provided as HTTP headers (an optional capability specified by OGC API Features), but support in Hakunapi is still an [upcoming feature](https://github.com/nlsfi/hakunapi/issues/91). diff --git a/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java b/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java index bc1e8268..2db38ee6 100644 --- a/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java +++ b/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java @@ -31,6 +31,11 @@ public ProjectionTransformer fromCRS84(int sridTo) throws Exception { return getTransformer(4258, sridTo); } + @Override + public String getWKT(int srid) throws Exception { + throw new UnsupportedOperationException("WKT definitions are not supported by JHe"); + } + private String getCacheKey(int sridFrom, int sridTo) { return sridFrom + "_" + sridTo; } diff --git a/src-community/hakunapi-tiles-generator/README.md b/src-community/hakunapi-tiles-generator/README.md index f5d98f93..ea327b36 100644 --- a/src-community/hakunapi-tiles-generator/README.md +++ b/src-community/hakunapi-tiles-generator/README.md @@ -207,7 +207,8 @@ the table, converted to WGS84 with the pyramid's own SRID; the centre is the middle of that extent. Nothing is assumed about where the data is. Converting a CRS other than EPSG:4326 needs a projection factory on the classpath (`-Dhakunapi.proj=`, default `fi.nls.hakunapi.proj.gt.GeoToolsProjectionTransformerFactory`, -as for a service configuration); without one, pass `--bbox`. +as for a service configuration); without one, pass `--bbox`. `hakunapi-proj-gt` +is bundled. The archive keeps the pyramid's own tile addressing. Every zoom must be a `2^z` by `2^z` matrix, because a PMTiles tile id is a Hilbert index over a diff --git a/src-community/hakunapi-tiles-generator/pom.xml b/src-community/hakunapi-tiles-generator/pom.xml index a27d7082..33d87e9d 100644 --- a/src-community/hakunapi-tiles-generator/pom.xml +++ b/src-community/hakunapi-tiles-generator/pom.xml @@ -23,6 +23,12 @@ fi.nls.hakunapi hakunapi-gpkg + + + fi.nls.hakunapi + hakunapi-proj-gt + runtime + fi.nls.hakunapi diff --git a/src-community/hakunapi-tiles-generator/src/main/java/fi/nls/hakunapi/tiles/generator/TilesGenerator.java b/src-community/hakunapi-tiles-generator/src/main/java/fi/nls/hakunapi/tiles/generator/TilesGenerator.java index 161a4f7d..67a922a1 100644 --- a/src-community/hakunapi-tiles-generator/src/main/java/fi/nls/hakunapi/tiles/generator/TilesGenerator.java +++ b/src-community/hakunapi-tiles-generator/src/main/java/fi/nls/hakunapi/tiles/generator/TilesGenerator.java @@ -16,6 +16,7 @@ import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.FilterParser; import fi.nls.hakunapi.core.config.HakunaConfigParser; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.cql2.text.CQL2Text; import fi.nls.hakunapi.gpkg.GPKG; import fi.nls.hakunapi.gpkg.GPKGSpatialRefSystems; @@ -94,7 +95,7 @@ private static int generate(Args parsed, GeneratorControl control, TileSources s GPKG.init(c); } GPKG.initTiles(c); - GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(tms.getSrid())); + GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(tms.getSrid(), projectionFactory(collections))); List existing = PyramidTables.existingTables(c, tables); if (!existing.isEmpty()) { @@ -133,6 +134,11 @@ private static VectorTileGenerator resolveGenerator(String type) { return generator; } + // The CRS definition comes from the same factory the collections are transformed with + private static ProjectionTransformerFactory projectionFactory(Map> collections) { + return collections.values().iterator().next().get(0).getProjectionTransformerFactory(); + } + private static void validateZooms(GeneratorControl control, TileMatrixSet tms, int minZoom, int maxZoom) { if (minZoom > maxZoom) { throw new IllegalArgumentException("--min-zoom " + minZoom + " is above --max-zoom " + maxZoom); diff --git a/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidBoundsTest.java b/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidBoundsTest.java index 3350acad..624652fe 100644 --- a/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidBoundsTest.java +++ b/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidBoundsTest.java @@ -17,6 +17,7 @@ import fi.nls.hakunapi.gpkg.GPKG; import fi.nls.hakunapi.gpkg.GPKGSpatialRefSystems; +import fi.nls.hakunapi.proj.gt.GeoToolsProjectionTransformerFactory; import fi.nls.hakunapi.tiles.TileMatrix; import fi.nls.hakunapi.tiles.TileMatrixSet; @@ -35,7 +36,7 @@ private Connection pyramid(int srid, String... tiles) throws Exception { Connection c = DriverManager.getConnection("jdbc:sqlite:" + path); GPKG.init(c); GPKG.initTiles(c); - GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(srid)); + GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(srid, new GeoToolsProjectionTransformerFactory())); PyramidTables.createTables(c, List.of("t"), new TileMatrixSet("T", "T", "urn:x", srid, matrices), 0, 2); try (Statement st = c.createStatement()) { for (String tile : tiles) { diff --git a/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidGeneratorTest.java b/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidGeneratorTest.java index 6b8773ef..f6ab335e 100644 --- a/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidGeneratorTest.java +++ b/src-community/hakunapi-tiles-generator/src/test/java/fi/nls/hakunapi/tiles/generator/PyramidGeneratorTest.java @@ -31,6 +31,7 @@ import fi.nls.hakunapi.core.config.HakunaConfigParser; import fi.nls.hakunapi.gpkg.GPKG; import fi.nls.hakunapi.gpkg.GPKGSpatialRefSystems; +import fi.nls.hakunapi.proj.gt.GeoToolsProjectionTransformerFactory; import fi.nls.hakunapi.tiles.TileBody; import fi.nls.hakunapi.tiles.TileMatrix; import fi.nls.hakunapi.tiles.TileMatrixSet; @@ -93,7 +94,7 @@ private Path generate(StubCollection[] stubs, GeneratorControl control, TileSour try (Connection c = open(out)) { GPKG.init(c); GPKG.initTiles(c); - GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID)); + GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID, new GeoToolsProjectionTransformerFactory())); PyramidTables.createTables(c, tables, tms, minZoom, maxZoom); PyramidGenerator pyramid = new PyramidGenerator(control, sources, tms, generator, @@ -222,7 +223,7 @@ public Optional generate(TileContext ctx, List collection try (Connection c = open(out)) { GPKG.init(c); GPKG.initTiles(c); - GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID)); + GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID, new GeoToolsProjectionTransformerFactory())); PyramidTables.createTables(c, tables, tms, 0, 3); PyramidGenerator pyramid = new PyramidGenerator(control, sources, tms, failing, VectorTileGeneratorSource.DEFAULT_MEDIA_TYPE, @@ -245,7 +246,7 @@ public void tileMetadataIsCompleteAndInitTilesIsIdempotent() throws Exception { GPKG.init(c); GPKG.initTiles(c); GPKG.initTiles(c); - GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID)); + GPKG.insertSpatialRefSys(c, GPKGSpatialRefSystems.get(SRID, new GeoToolsProjectionTransformerFactory())); PyramidTables.createTables(c, List.of("tieviiva"), scheme(), 1, 2); assertTrue(PyramidTables.existingTables(c, List.of("tieviiva", "x")).equals(List.of("tieviiva"))); try (Statement st = c.createStatement()) { diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java index 69f89da8..81e7c1d6 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java @@ -5,5 +5,9 @@ public interface ProjectionTransformerFactory { public ProjectionTransformer getTransformer(int fromSRID, int toSRID) throws Exception; public ProjectionTransformer toCRS84(int fromSRID) throws Exception; public ProjectionTransformer fromCRS84(int toSRID) throws Exception; + /** + * @return WKT 1 definition of the CRS, required for every srid this factory can transform + */ + public String getWKT(int srid) throws Exception; } diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java index e2be1e76..06ce34eb 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java @@ -73,6 +73,11 @@ public ProjectionTransformer toCRS84(int fromSRID) { public ProjectionTransformer fromCRS84(int toSRID) { return NOPProjectionTransformer.INSTANCE; } + + @Override + public String getWKT(int srid) { + throw new UnsupportedOperationException(); + } }); service = new FeatureServiceConfig() { diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java index e85b223d..b649fb48 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java @@ -60,6 +60,11 @@ public ProjectionTransformer toCRS84(int fromSRID) { public ProjectionTransformer fromCRS84(int toSRID) { return NOPProjectionTransformer.INSTANCE; } + + @Override + public String getWKT(int srid) { + throw new UnsupportedOperationException(); + } }); service = new FeatureServiceConfig() { diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java index 73650aba..4a1cb847 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java @@ -114,6 +114,11 @@ public ProjectionTransformer toCRS84(int fromSRID) { public ProjectionTransformer fromCRS84(int toSRID) { throw new UnsupportedOperationException(); } + + @Override + public String getWKT(int srid) { + throw new UnsupportedOperationException(); + } }); HakunaPropertyGeometry prop = new HakunaPropertyGeometry("geometry", "table", "geom", false, null, new int[0], 3067, 2, HakunaPropertyWriters.HIDDEN); diff --git a/src/hakunapi-gpkg/pom.xml b/src/hakunapi-gpkg/pom.xml index 9d36294b..0f307588 100644 --- a/src/hakunapi-gpkg/pom.xml +++ b/src/hakunapi-gpkg/pom.xml @@ -30,11 +30,6 @@ sqlite-jdbc ${sqlite-jdbc.version} - - org.geotools - gt-epsg-hsql - ${geotools.version} - junit junit diff --git a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGFeatureCollectionWriter.java b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGFeatureCollectionWriter.java index 86e9d325..a0af9c3a 100644 --- a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGFeatureCollectionWriter.java +++ b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGFeatureCollectionWriter.java @@ -111,7 +111,7 @@ public void endFeatureCollection() throws Exception { } insert.close(); GPKGGeometryColumn geometryColumn = createGeometryColumn(); - GPKGSpatialRefSys srs = GPKGSpatialRefSystems.get(srid.getSrid()); + GPKGSpatialRefSys srs = GPKGSpatialRefSystems.get(srid.getSrid(), ft.getProjectionTransformerFactory()); GPKG.insertSpatialRefSys(c, srs); GPKGFeaturesTable tableEntry = new GPKGFeaturesTable(tableName, ft.getTitle(), ft.getDescription(), envelope, srs.getSrsId()); GPKG.insertFeaturesTableEntry(c, tableEntry); diff --git a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java index d777c59c..db80a7d0 100644 --- a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java +++ b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java @@ -1,11 +1,9 @@ package fi.nls.hakunapi.gpkg; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; -import org.geotools.referencing.CRS; - +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.core.schemas.Crs; public class GPKGSpatialRefSystems { @@ -16,7 +14,7 @@ private GPKGSpatialRefSystems() {} public static final GPKGSpatialRefSys UNDEFINED_CARTESIAN = new GPKGSpatialRefSys("Undefined Cartesian", -1, "NONE", -1, "undefined", "undefined"); public static final GPKGSpatialRefSys UNDEFINED_GEOGRAPHIC = new GPKGSpatialRefSys("Undefined Geographic", 0, "NONE", 0, "undefined", "undefined"); - private static final Map BY_SRID = new HashMap<>(); + private static final Map BY_SRID = new ConcurrentHashMap<>(); static { register(WGS84); register(UNDEFINED_CARTESIAN); @@ -27,21 +25,20 @@ public static void register(GPKGSpatialRefSys sys) { BY_SRID.put(sys.getSrsId(), sys); } - public static GPKGSpatialRefSys get(int srid) { + public static GPKGSpatialRefSys get(int srid, ProjectionTransformerFactory proj) { if (srid == Crs.CRS84_SRID) { return WGS84; } - return BY_SRID.computeIfAbsent(srid, GPKGSpatialRefSystems::epsg); + return BY_SRID.computeIfAbsent(srid, __ -> epsg(srid, proj)); } - private static GPKGSpatialRefSys epsg(int srid) { + private static GPKGSpatialRefSys epsg(int srid, ProjectionTransformerFactory proj) { try { String srsName = "EPSG:" + srid; - CoordinateReferenceSystem crs = CRS.decode(srsName, true); - String definition = crs.toWKT(); + String definition = proj.getWKT(srid); return new GPKGSpatialRefSys(srsName, srid, "EPSG", srid, definition, srsName); } catch (Exception e) { - throw new RuntimeException("Could not decode CRS for srid: " + srid); + throw new RuntimeException("Could not decode CRS for srid: " + srid, e); } } diff --git a/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java b/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java index 7a4b9593..5bb4d55f 100644 --- a/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java +++ b/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java @@ -73,6 +73,11 @@ public ProjectionTransformer fromCRS84(int sridTo) throws Exception { return getProjectionTransformer(Crs.CRS84_SRID, CRS84, sridTo, getHorizontalCRS(sridTo)); } + @Override + public String getWKT(int srid) throws Exception { + return CRS.decode("EPSG:" + srid, true).toWKT(); + } + private static CoordinateReferenceSystem getHorizontalCRS(int srid) throws Exception { CoordinateReferenceSystem crs = CRS.decode("EPSG:" + srid, true); if (crs instanceof CompoundCRS) { diff --git a/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java b/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java index 84e5455b..8ac3b43a 100644 --- a/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java +++ b/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java @@ -1,6 +1,7 @@ package fi.nls.hakunapi.proj.gt; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Ignore; import org.junit.Test; @@ -20,6 +21,14 @@ public void testCRS84to3067() throws Exception { assertEquals(6651411.193370228, coords[1], 1e-8); } + @Test + public void testWKT3067() throws Exception { + ProjectionTransformerFactory fact = new GeoToolsProjectionTransformerFactory(); + String wkt = fact.getWKT(3067); + assertTrue(wkt, wkt.startsWith("PROJCS[\"EUREF-FIN / TM35FIN(E,N)\"")); + assertTrue(wkt, wkt.endsWith("AUTHORITY[\"EPSG\",\"3067\"]]")); + } + @Test @Ignore("Issue with Java asserts with 4258 (lon,lat) -> 10690 (lon,lat) step being NO-OP") public void test4258to3067() throws Exception { From b3ef5f4b02798470deb2027f781d37ab1aaf7175 Mon Sep 17 00:00:00 2001 From: jampukka Date: Fri, 23 Oct 2026 14:45:22 +0300 Subject: [PATCH 2/3] Make the WKT axis order explicit with a longitudeFirst flag --- CHANGELOG.md | 2 +- docs/overview/modules.md | 2 +- .../proj/jhe/JHeProjectionTransformerFactory.java | 2 +- .../core/projection/ProjectionTransformerFactory.java | 3 ++- .../nls/hakunapi/core/param/BboxAntimeridianTest.java | 2 +- .../fi/nls/hakunapi/core/param/BboxCrsParamTest.java | 2 +- .../property/simple/HakunaPropertyGeometryTest.java | 2 +- .../fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java | 3 ++- .../proj/gt/GeoToolsProjectionTransformerFactory.java | 4 ++-- .../proj/gt/GeoToolsProjectionTransformerTest.java | 11 ++++++++++- 10 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c29da8..19349bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Breaking changes (migration guide) -- `ProjectionTransformerFactory` has a new required method `String getWKT(int srid)`, the WKT 1 definition of a CRS the factory supports. Custom implementations must add it. `hakunapi-gpkg` uses it for `gpkg_spatial_ref_sys` and no longer depends on GeoTools (`gt-epsg-hsql`) itself; the GeoPackage output now needs a projection factory that provides WKT, as `hakunapi-proj-gt` does. `JHeProjectionTransformerFactory` (community) throws `UnsupportedOperationException` for now. +- `ProjectionTransformerFactory` has a new required method `String getWKT(int srid, boolean longitudeFirst)`, the WKT 1 definition of a CRS the factory supports, optionally with x/y axis order forced. Custom implementations must add it. `hakunapi-gpkg` uses it for `gpkg_spatial_ref_sys` and no longer depends on GeoTools (`gt-epsg-hsql`) itself; the GeoPackage output now needs a projection factory that provides WKT, as `hakunapi-proj-gt` does. `JHeProjectionTransformerFactory` (community) throws `UnsupportedOperationException` for now. ### Internationalization / Localization diff --git a/docs/overview/modules.md b/docs/overview/modules.md index 2f07d30d..4588f11f 100644 --- a/docs/overview/modules.md +++ b/docs/overview/modules.md @@ -323,7 +323,7 @@ The map on the items and item pages is a demo feature. A background tile layer i ### GeoPackage -The [GeoPackage (GPKG)](https://www.geopackage.org/) output format (the media type `application/geopackage+vnd.sqlite3`) is supported by the `hakunapi-gpgk` module. When needed it must be enabled using a configuration like `formats=geojson,gpgk`. The implementation supports outputting same feature, feature collection, property and geometry objects (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and GeometryCollection) as GeoJSON writer too. The CRS definition (WKT) written to `gpkg_spatial_ref_sys` comes from the collection's projection factory (`ProjectionTransformerFactory.getWKT`), so the module itself has no GeoTools dependency. +The [GeoPackage (GPKG)](https://www.geopackage.org/) output format (the media type `application/geopackage+vnd.sqlite3`) is supported by the `hakunapi-gpgk` module. When needed it must be enabled using a configuration like `formats=geojson,gpgk`. The implementation supports outputting same feature, feature collection, property and geometry objects (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon and GeometryCollection) as GeoJSON writer too. The CRS definition (WKT) written to `gpkg_spatial_ref_sys` comes from the collection's projection factory (`ProjectionTransformerFactory.getWKT`, with x/y axis order like the geometries), so the module itself has no GeoTools dependency. Currently [OGC API Features](https://ogcapi.ogc.org/features/) does not specify anything related to GPGK. Some feature service metadata (like next links) cannot be written in content payload like in the case of the default GeoJSON output. Such metadata should be provided as HTTP headers (an optional capability specified by OGC API Features), but support in Hakunapi is still an [upcoming feature](https://github.com/nlsfi/hakunapi/issues/91). diff --git a/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java b/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java index 2db38ee6..fc34a11d 100644 --- a/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java +++ b/src-community/hakunapi-proj-jhe/src/main/java/fi/nls/hakunapi/proj/jhe/JHeProjectionTransformerFactory.java @@ -32,7 +32,7 @@ public ProjectionTransformer fromCRS84(int sridTo) throws Exception { } @Override - public String getWKT(int srid) throws Exception { + public String getWKT(int srid, boolean longitudeFirst) throws Exception { throw new UnsupportedOperationException("WKT definitions are not supported by JHe"); } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java index 81e7c1d6..ce6f220b 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/projection/ProjectionTransformerFactory.java @@ -6,8 +6,9 @@ public interface ProjectionTransformerFactory { public ProjectionTransformer toCRS84(int fromSRID) throws Exception; public ProjectionTransformer fromCRS84(int toSRID) throws Exception; /** + * @param longitudeFirst force x/y (easting/longitude first) axis order instead of the authority's * @return WKT 1 definition of the CRS, required for every srid this factory can transform */ - public String getWKT(int srid) throws Exception; + public String getWKT(int srid, boolean longitudeFirst) throws Exception; } diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java index 06ce34eb..23bd8a53 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxAntimeridianTest.java @@ -75,7 +75,7 @@ public ProjectionTransformer fromCRS84(int toSRID) { } @Override - public String getWKT(int srid) { + public String getWKT(int srid, boolean longitudeFirst) { throw new UnsupportedOperationException(); } }); diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java index b649fb48..89b78d29 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/BboxCrsParamTest.java @@ -62,7 +62,7 @@ public ProjectionTransformer fromCRS84(int toSRID) { } @Override - public String getWKT(int srid) { + public String getWKT(int srid, boolean longitudeFirst) { throw new UnsupportedOperationException(); } }); diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java index 4a1cb847..8992faa7 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/property/simple/HakunaPropertyGeometryTest.java @@ -116,7 +116,7 @@ public ProjectionTransformer fromCRS84(int toSRID) { } @Override - public String getWKT(int srid) { + public String getWKT(int srid, boolean longitudeFirst) { throw new UnsupportedOperationException(); } }); diff --git a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java index db80a7d0..7b60ea71 100644 --- a/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java +++ b/src/hakunapi-gpkg/src/main/java/fi/nls/hakunapi/gpkg/GPKGSpatialRefSystems.java @@ -35,7 +35,8 @@ public static GPKGSpatialRefSys get(int srid, ProjectionTransformerFactory proj) private static GPKGSpatialRefSys epsg(int srid, ProjectionTransformerFactory proj) { try { String srsName = "EPSG:" + srid; - String definition = proj.getWKT(srid); + // Geometries are always x/y in a GeoPackage, keep the definition consistent with them + String definition = proj.getWKT(srid, true); return new GPKGSpatialRefSys(srsName, srid, "EPSG", srid, definition, srsName); } catch (Exception e) { throw new RuntimeException("Could not decode CRS for srid: " + srid, e); diff --git a/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java b/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java index 5bb4d55f..5a68ba56 100644 --- a/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java +++ b/src/hakunapi-proj-gt/src/main/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerFactory.java @@ -74,8 +74,8 @@ public ProjectionTransformer fromCRS84(int sridTo) throws Exception { } @Override - public String getWKT(int srid) throws Exception { - return CRS.decode("EPSG:" + srid, true).toWKT(); + public String getWKT(int srid, boolean longitudeFirst) throws Exception { + return CRS.decode("EPSG:" + srid, longitudeFirst).toWKT(); } private static CoordinateReferenceSystem getHorizontalCRS(int srid) throws Exception { diff --git a/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java b/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java index 8ac3b43a..1290bf45 100644 --- a/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java +++ b/src/hakunapi-proj-gt/src/test/java/fi/nls/hakunapi/proj/gt/GeoToolsProjectionTransformerTest.java @@ -24,11 +24,20 @@ public void testCRS84to3067() throws Exception { @Test public void testWKT3067() throws Exception { ProjectionTransformerFactory fact = new GeoToolsProjectionTransformerFactory(); - String wkt = fact.getWKT(3067); + String wkt = fact.getWKT(3067, true); assertTrue(wkt, wkt.startsWith("PROJCS[\"EUREF-FIN / TM35FIN(E,N)\"")); assertTrue(wkt, wkt.endsWith("AUTHORITY[\"EPSG\",\"3067\"]]")); } + @Test + public void testWKTAxisOrder4258() throws Exception { + ProjectionTransformerFactory fact = new GeoToolsProjectionTransformerFactory(); + String xy = fact.getWKT(4258, true); + String authority = fact.getWKT(4258, false); + assertTrue(xy, xy.indexOf("EAST]") < xy.indexOf("NORTH]")); + assertTrue(authority, authority.indexOf("NORTH]") < authority.indexOf("EAST]")); + } + @Test @Ignore("Issue with Java asserts with 4258 (lon,lat) -> 10690 (lon,lat) step being NO-OP") public void test4258to3067() throws Exception { From 1cbe2ebb827638497c21ef84a555600a48fc70f3 Mon Sep 17 00:00:00 2001 From: jampukka Date: Fri, 23 Oct 2026 14:37:12 +0300 Subject: [PATCH 3/3] Exclude GeoTools dependencies hakunapi never loads from proj-gt --- CHANGELOG.md | 4 ++++ docs/overview/modules.md | 2 +- src/hakunapi-proj-gt/pom.xml | 31 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19349bd0..f35e53f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,10 @@ - `servers` is now trimmed like the other list keys: `servers=dev, prod` works instead of failing startup with a `NullPointerException`. - PostGIS source: the keys `collections..id.alias`, `geometry.alias` and `properties.

.alias` are removed. They were a not-working and unfinished idea (`properties.

.alias` made startup fail with a `NullPointerException`); they are now ignored. +### Library updates + +- `hakunapi-proj-gt` excludes `gt-epsg-hsql`'s transitive `net.opengis.ows`, `org.w3.xlink`, the three `org.eclipse.emf` jars, `imagen-core` and `commons-lang3` (~4 MB). GeoTools references them only from ISO citation metadata, raster parameters and URN/PROJ formatting, none of which hakunapi uses; transformations and WKT are unchanged. + ## 2.0.0 For a full list of changes see: https://github.com/nlsfi/hakunapi/milestone/17 diff --git a/docs/overview/modules.md b/docs/overview/modules.md index 4588f11f..449e9f3a 100644 --- a/docs/overview/modules.md +++ b/docs/overview/modules.md @@ -72,7 +72,7 @@ The table below lists all direct third-party dependencies declared in the Hakuna | Library | Version | Purpose | Notes | | --- | --- | --- | --- | | JTS Topology Suite | 1.20.0 | Geometry model, spatial predicates | | -| GeoTools | 35.1 | proj-gt: `gt-epsg-hsql` provides EPSG CRS database (HSQL-backed) for CRS definitions and reprojection | **To consider:** hakunapi-proj-proj (PROJ) [Issue #143](https://github.com/nlsfi/hakunapi/issues/143) to replace hakunapi-proj-gt for CRS definitions and reprojection? | +| GeoTools | 35.1 | proj-gt: `gt-epsg-hsql` provides EPSG CRS database (HSQL-backed) for CRS definitions and reprojection. Transitive OWS/xlink/EMF, ImageN and commons-lang3 are excluded: only GeoTools code paths hakunapi does not use reference them | **To consider:** hakunapi-proj-proj (PROJ) [Issue #143](https://github.com/nlsfi/hakunapi/issues/143) to replace hakunapi-proj-gt for CRS definitions and reprojection? | #### Database connectivity diff --git a/src/hakunapi-proj-gt/pom.xml b/src/hakunapi-proj-gt/pom.xml index fe78f6f9..6805f617 100644 --- a/src/hakunapi-proj-gt/pom.xml +++ b/src/hakunapi-proj-gt/pom.xml @@ -25,6 +25,37 @@ org.geotools gt-epsg-hsql ${geotools.version} + + + + org.geotools.ogc + net.opengis.ows + + + org.geotools.ogc + org.w3.xlink + + + org.eclipse.emf + org.eclipse.emf.common + + + org.eclipse.emf + org.eclipse.emf.ecore + + + org.eclipse.emf + org.eclipse.emf.ecore.xmi + + + org.eclipse.imagen + imagen-core + + + org.apache.commons + commons-lang3 + + junit