Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Breaking changes (migration guide)

- `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

- **Language negotiation for descriptive elements.** The landing page,
Expand Down Expand Up @@ -84,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>.id.alias`, `geometry.alias` and `properties.<p>.alias` are removed. They were a not-working and unfinished idea (`properties.<p>.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
Expand Down
6 changes: 3 additions & 3 deletions docs/overview/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <jar> fi.nls.hakunapi.tiles.generator.TilesGenerator <service.properties> <control.properties> <output.gpkg>`), and `PMTilesConvert` the same way with its main class and `<pyramid.gpkg> <table> <output.pmtiles>`. 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 <jar> fi.nls.hakunapi.tiles.generator.TilesGenerator <service.properties> <control.properties> <output.gpkg>`), and `PMTilesConvert` the same way with its main class and `<pyramid.gpkg> <table> <output.pmtiles>`. 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)

Expand Down Expand Up @@ -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`, 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).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public ProjectionTransformer fromCRS84(int sridTo) throws Exception {
return getTransformer(4258, sridTo);
}

@Override
public String getWKT(int srid, boolean longitudeFirst) throws Exception {
throw new UnsupportedOperationException("WKT definitions are not supported by JHe");
}

private String getCacheKey(int sridFrom, int sridTo) {
return sridFrom + "_" + sridTo;
}
Expand Down
3 changes: 2 additions & 1 deletion src-community/hakunapi-tiles-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<class>`, 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
Expand Down
6 changes: 6 additions & 0 deletions src-community/hakunapi-tiles-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
<groupId>fi.nls.hakunapi</groupId>
<artifactId>hakunapi-gpkg</artifactId>
</dependency>
<!-- Default projection factory, loaded by class name and not imported -->
<dependency>
<groupId>fi.nls.hakunapi</groupId>
<artifactId>hakunapi-proj-gt</artifactId>
<scope>runtime</scope>
</dependency>
<!-- PMTilesWriter, used by PMTilesConvert -->
<dependency>
<groupId>fi.nls.hakunapi</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> existing = PyramidTables.existingTables(c, tables);
if (!existing.isEmpty()) {
Expand Down Expand Up @@ -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<String, List<FeatureType>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -222,7 +223,7 @@ public Optional<TileBody> generate(TileContext ctx, List<FeatureType> 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,
Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ 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;
/**
* @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, boolean longitudeFirst) throws Exception;

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public ProjectionTransformer toCRS84(int fromSRID) {
public ProjectionTransformer fromCRS84(int toSRID) {
return NOPProjectionTransformer.INSTANCE;
}

@Override
public String getWKT(int srid, boolean longitudeFirst) {
throw new UnsupportedOperationException();
}
});

service = new FeatureServiceConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public ProjectionTransformer toCRS84(int fromSRID) {
public ProjectionTransformer fromCRS84(int toSRID) {
return NOPProjectionTransformer.INSTANCE;
}

@Override
public String getWKT(int srid, boolean longitudeFirst) {
throw new UnsupportedOperationException();
}
});

service = new FeatureServiceConfig() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ public ProjectionTransformer toCRS84(int fromSRID) {
public ProjectionTransformer fromCRS84(int toSRID) {
throw new UnsupportedOperationException();
}

@Override
public String getWKT(int srid, boolean longitudeFirst) {
throw new UnsupportedOperationException();
}
});
HakunaPropertyGeometry prop = new HakunaPropertyGeometry("geometry", "table", "geom", false, null,
new int[0], 3067, 2, HakunaPropertyWriters.HIDDEN);
Expand Down
5 changes: 0 additions & 5 deletions src/hakunapi-gpkg/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@
<artifactId>sqlite-jdbc</artifactId>
<version>${sqlite-jdbc.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<Integer, GPKGSpatialRefSys> BY_SRID = new HashMap<>();
private static final Map<Integer, GPKGSpatialRefSys> BY_SRID = new ConcurrentHashMap<>();
static {
register(WGS84);
register(UNDEFINED_CARTESIAN);
Expand All @@ -27,21 +25,21 @@ 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();
// 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);
throw new RuntimeException("Could not decode CRS for srid: " + srid, e);
}
}

Expand Down
31 changes: 31 additions & 0 deletions src/hakunapi-proj-gt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
<!-- Only referenced by GeoTools code paths hakunapi does not use (ISO citation metadata, raster parameters, URN/PROJ formatting) -->
<exclusions>
<exclusion>
<groupId>org.geotools.ogc</groupId>
<artifactId>net.opengis.ows</artifactId>
</exclusion>
<exclusion>
<groupId>org.geotools.ogc</groupId>
<artifactId>org.w3.xlink</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.common</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.xmi</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.imagen</groupId>
<artifactId>imagen-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
Expand Down
Loading
Loading