From f6b1a62eb421b4ffde77e45e82767976ff9b4a7f Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Tue, 12 May 2026 15:28:17 +0200 Subject: [PATCH 1/7] add configurable collection/tileset extents --- .../features/domain/CollectionExtent.java | 21 ++++++++++ .../domain/FeatureProviderDataV2.java | 16 ++++++++ .../domain/FeatureTypeConfiguration.java | 8 ++++ .../tiles/app/TileProviderFeatures.java | 40 ++++++++++++------- .../tiles/app/TileProviderHttp.java | 8 ++++ .../tiles/app/TileProviderMbTiles.java | 23 +++++++++-- .../tiles/domain/TilesetCommon.java | 9 +++++ .../tiles/domain/TilesetFeaturesDefaults.java | 22 ++++++++++ .../tiles/domain/TilesetHttpDefaults.java | 22 ++++++++++ .../tiles/domain/TilesetMbTilesDefaults.java | 21 ++++++++++ 10 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java new file mode 100644 index 000000000..47974c9f0 --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java @@ -0,0 +1,21 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain; + +import de.ii.xtraplatform.cql.domain.Interval; +import de.ii.xtraplatform.crs.domain.BoundingBox; +import java.util.Optional; +import org.immutables.value.Value; + +/** Extent object for spatial and temporal extents. */ +@Value.Immutable +public interface CollectionExtent { + Optional getSpatial(); + + Optional getTemporal(); +} diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java index 006b9affe..4d8fc65e2 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java @@ -184,6 +184,14 @@ default long getEntitySchemaVersion() { @Override Optional getAuto(); + /** + * @langEn Optional spatial and temporal extent for all types in this provider. If set, disables + * automatic calculation. + * @langDe Optionaler räumlicher und zeitlicher Extent für alle Types dieses Providers. Wenn + * gesetzt, wird keine automatische Berechnung durchgeführt. + */ + Optional getExtent(); + // custom builder to automatically use keys of types as name of FeatureTypeV2 abstract class Builder> implements EntityDataBuilder { @@ -233,5 +241,13 @@ public T putFragments2(String key, ImmutableFeatureSchema.Builder builder) { @JsonProperty("extensions") public abstract T addAllExtensions(Iterable elements); + + /** + * @langEn Optional spatial and temporal extent for all types in this provider. If set, disables + * automatic calculation. + * @langDe Optionaler räumlicher und zeitlicher Extent für alle Types dieses Providers. Wenn + * gesetzt, wird keine automatische Berechnung durchgeführt. + */ + public abstract T extent(CollectionExtent extent); } } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java index 968af0820..0ea035140 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java @@ -35,4 +35,12 @@ public interface FeatureTypeConfiguration { * @default "" */ Optional getDescription(); + + /** + * @langEn Optional spatial and temporal extent for this type. If set, disables automatic + * calculation for this type. + * @langDe Optionaler räumlicher und zeitlicher Extent für diesen Type. Wenn gesetzt, wird keine + * automatische Berechnung für diesen Type durchgeführt. + */ + Optional getExtent(); } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java index 329ff3f32..b103f0f62 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java @@ -999,20 +999,32 @@ private TilesetMetadata loadMetadata(TilesetFeatures tileset) { getLayers(tileset).stream() .map(id -> tileGenerator.getVectorSchema(id, FeatureEncoderMVT.FORMAT)) .collect(Collectors.toList()); - Optional bounds = - getLayers(tileset).stream() - .map(tileGenerator::getBounds) - .reduce( - Optional.empty(), - (a, b) -> { - if (b.isEmpty()) { - return a; - } - if (a.isPresent()) { - return Optional.of(BoundingBox.merge(b.get(), a.get())); - } - return b; - }); + + boolean forceCompute = getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); + Optional configuredExtent = + forceCompute + ? Optional.empty() + : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + + Optional bounds; + if (configuredExtent.isPresent()) { + bounds = configuredExtent; + } else { + bounds = + getLayers(tileset).stream() + .map(tileGenerator::getBounds) + .reduce( + Optional.empty(), + (a, b) -> { + if (b.isEmpty()) { + return a; + } + if (a.isPresent()) { + return Optional.of(BoundingBox.merge(b.get(), a.get())); + } + return b; + }); + } return ImmutableTilesetMetadata.builder() .addEncodings(TilesFormat.MVT) diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java index 8b924f3c6..3cf3926e6 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java @@ -11,6 +11,7 @@ import dagger.assisted.Assisted; import dagger.assisted.AssistedInject; import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.Entity; import de.ii.xtraplatform.entities.domain.Entity.SubType; import de.ii.xtraplatform.features.domain.ProviderData; @@ -117,6 +118,12 @@ private void loadMetadata() { } private TilesetMetadata loadMetadata(TilesetHttp tileset) { + boolean forceCompute = getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); + Optional bounds = + forceCompute + ? Optional.empty() + : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + return ImmutableTilesetMetadata.builder() .encodings( tileset.getEncodings().isEmpty() @@ -127,6 +134,7 @@ private TilesetMetadata loadMetadata(TilesetHttp tileset) { ? getData().getTilesetDefaults().getLevels() : tileset.getLevels()) .center(tileset.getCenter().or(() -> getData().getTilesetDefaults().getCenter())) + .bounds(bounds) .build(); } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java index d0c33dc53..5b6a54900 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java @@ -177,12 +177,26 @@ private void loadMetadata(Map tilesetSources) { (key, path) -> { Tuple tilesetKey = toTuple(key); - metadata.put(tilesetKey.first(), loadMetadata(tilesetKey.second(), path)); + TilesetMbTiles tileset = + getData() + .getTilesets() + .get(tilesetKey.first()) + .mergeDefaults(getData().getTilesetDefaults()); + boolean forceCompute = + getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); + Optional configuredExtent = + forceCompute + ? Optional.empty() + : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + + metadata.put( + tilesetKey.first(), loadMetadata(tilesetKey.second(), path, configuredExtent)); tmsRanges.put(tilesetKey.first(), metadata.get(tilesetKey.first()).getTmsRanges()); }); } - private TilesetMetadata loadMetadata(String tms, Path path) { + private TilesetMetadata loadMetadata( + String tms, Path path, Optional configuredExtent) { try { MbtilesMetadata metadata = new MbtilesTileset(path, false).getMetadata(); TileMatrixSet tileMatrixSet = @@ -210,11 +224,14 @@ private TilesetMetadata loadMetadata(String tms, Path path) { tms, new ImmutableMinMax.Builder().min(minzoom).max(maxzoom).getDefault(defzoom).build()); List bbox = metadata.getBounds(); - Optional bounds = + Optional boundsFromMetadata = bbox.size() == 4 ? Optional.of( BoundingBox.of(bbox.get(0), bbox.get(1), bbox.get(2), bbox.get(3), OgcCrs.CRS84)) : Optional.empty(); + // Configured extent takes priority; fall back to bounds from MBTiles metadata + Optional bounds = + configuredExtent.isPresent() ? configuredExtent : boundsFromMetadata; TilesFormat format = metadata.getFormat(); List vectorSchemas = metadata.getVectorLayers().stream() diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java index 870bdeecb..107ab7104 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java @@ -7,6 +7,7 @@ */ package de.ii.xtraplatform.tiles.domain; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; import de.ii.xtraplatform.tiles.domain.ImmutableMinMax.Builder; import java.util.Optional; @@ -24,4 +25,12 @@ public interface TilesetCommon extends TilesetCommonDefaults { @Override Optional getCenter(); + + /** + * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip + * bounding box. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert + * als Clip-BoundingBox verwendet. + */ + Optional getExtent(); } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java index f650d3f42..67e574ac4 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java @@ -8,9 +8,11 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.Buildable; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableBuilder; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import java.util.Optional; import org.immutables.value.Value; /** @@ -35,4 +37,24 @@ default ImmutableTilesetFeaturesDefaults.Builder getBuilder() { } abstract class Builder implements BuildableBuilder {} + + /** + * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip + * bounding box instead of computing it. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert + * als Clip-BoundingBox verwendet, statt ihn zu berechnen. + */ + Optional getExtent(); + + /** + * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is + * set globally. If false, a fixed value is always used. If not set, the global or provider + * logic applies. + * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global + * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht + * gesetzt, gilt die globale oder Provider-Logik. + */ + default Optional getSpatialExtentComputed() { + return Optional.empty(); + } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java index acd094cd5..93da07a24 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java @@ -8,9 +8,11 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.Buildable; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableBuilder; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import java.util.Optional; import org.immutables.value.Value; /** @@ -31,5 +33,25 @@ default ImmutableTilesetHttpDefaults.Builder getBuilder() { return new ImmutableTilesetHttpDefaults.Builder().from(this); } + /** + * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip + * bounding box instead of falling back to TileMatrixSet bounds. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert + * als Clip-BoundingBox verwendet, statt auf TileMatrixSet-Bounds zurückzufallen. + */ + Optional getExtent(); + + /** + * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is + * set globally. If false, a fixed value is always used. If not set, the global or provider + * logic applies. + * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global + * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht + * gesetzt, gilt die globale oder Provider-Logik. + */ + default Optional getSpatialExtentComputed() { + return Optional.empty(); + } + abstract class Builder implements BuildableBuilder {} } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java index ab183c13f..8f51b6fa6 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java @@ -8,6 +8,7 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.docs.DocIgnore; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; import de.ii.xtraplatform.tiles.domain.ImmutableMinMax.Builder; @@ -36,4 +37,24 @@ public interface TilesetMbTilesDefaults extends TilesetCommonDefaults { default String getTileMatrixSet() { return "WebMercatorQuad"; } + + /** + * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip + * bounding box instead of falling back to MBTiles metadata. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert + * als Clip-BoundingBox verwendet, statt auf MBTiles-Metadaten zurückzufallen. + */ + Optional getExtent(); + + /** + * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is + * set globally. If false, a fixed value is always used. If not set, the global or provider + * logic applies. + * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global + * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht + * gesetzt, gilt die globale oder Provider-Logik. + */ + default Optional getSpatialExtentComputed() { + return Optional.empty(); + } } From c8c630ecdf4c0b71a0e9a1734c7886a251e79859 Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Fri, 15 May 2026 13:01:47 +0200 Subject: [PATCH 2/7] add TemporalExtent and include extent in FeatureSchema --- .../features/domain/CollectionExtent.java | 5 ++-- .../features/domain/FeatureSchema.java | 8 ++++++ .../features/domain/TemporalExtent.java | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java index 47974c9f0..cc0d7d7e6 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java @@ -7,15 +7,16 @@ */ package de.ii.xtraplatform.features.domain; -import de.ii.xtraplatform.cql.domain.Interval; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import de.ii.xtraplatform.crs.domain.BoundingBox; import java.util.Optional; import org.immutables.value.Value; /** Extent object for spatial and temporal extents. */ @Value.Immutable +@JsonDeserialize(builder = ImmutableCollectionExtent.Builder.class) public interface CollectionExtent { Optional getSpatial(); - Optional getTemporal(); + Optional getTemporal(); } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java index eb28bab64..1fe3e1f4b 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java @@ -221,6 +221,14 @@ default Type getType() { */ Optional getDescription(); + /** + * @langEn Optional spatial and temporal extent for this feature type. If set, automatic + * calculation for this type can be skipped by consuming applications. + * @langDe Optionaler räumlicher und zeitlicher Extent für diesen Feature-Type. Wenn gesetzt, kann + * die automatische Berechnung für diesen Type in konsumierenden Anwendungen entfallen. + */ + Optional getExtent(); + /** * @langEn The unit of measurement of the value, only relevant for numeric properties. * @langDe Die Maßeinheit des Wertes, nur relevant bei numerischen Eigenschaften. diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java new file mode 100644 index 000000000..87b237ccb --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import javax.annotation.Nullable; +import org.immutables.value.Value; + +/** Temporal extent with start and end as Unix timestamp in milliseconds. */ +@Value.Immutable +@JsonDeserialize(builder = ImmutableTemporalExtent.Builder.class) +public interface TemporalExtent { + + /** + * Start of the temporal extent as Unix timestamp in milliseconds, or {@code null} for open start. + */ + @Nullable + Long getStart(); + + /** End of the temporal extent as Unix timestamp in milliseconds, or {@code null} for open end. */ + @Nullable + Long getEnd(); +} From 645fc5252175cd0b9b94159aa989640c66651b41 Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Wed, 20 May 2026 19:12:10 +0200 Subject: [PATCH 3/7] add spatial/temporal extent types and usage --- .../features/gml/app/FeatureProviderWfs.java | 86 +++++++++++++++++- .../graphql/app/FeatureProviderGraphQl.java | 87 ++++++++++++++++++- .../sql/domain/FeatureProviderSql.java | 86 ++++++++++++++++++ .../domain/FeatureProviderDataV2.java | 10 +-- .../features/domain/FeatureSchema.java | 8 +- .../domain/FeatureTypeConfiguration.java | 2 +- ...tionExtent.java => FeatureTypeExtent.java} | 7 +- .../features/domain/SpatialExtent.java | 66 ++++++++++++++ .../features/domain/TemporalExtent.java | 55 ++++++++++-- .../tiles/app/TileProviderFeatures.java | 45 +++++++++- .../tiles/app/TileProviderHttp.java | 34 +++++++- .../tiles/app/TileProviderMbTiles.java | 35 ++++++-- .../tiles/domain/TilesetCommon.java | 10 +-- .../tiles/domain/TilesetFeaturesDefaults.java | 22 +---- .../tiles/domain/TilesetHttpDefaults.java | 22 +---- .../tiles/domain/TilesetMbTilesDefaults.java | 22 +---- 16 files changed, 493 insertions(+), 104 deletions(-) rename xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/{CollectionExtent.java => FeatureTypeExtent.java} (74%) create mode 100644 xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java index 4620e2d8f..aaaa22fc8 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java @@ -46,11 +46,14 @@ import de.ii.xtraplatform.features.domain.FeatureStream; import de.ii.xtraplatform.features.domain.FeatureStreamImpl; import de.ii.xtraplatform.features.domain.FeatureTokenDecoder; +import de.ii.xtraplatform.features.domain.FeatureTypeExtent; import de.ii.xtraplatform.features.domain.Metadata; import de.ii.xtraplatform.features.domain.ProviderData; import de.ii.xtraplatform.features.domain.ProviderExtensionRegistry; import de.ii.xtraplatform.features.domain.Query; import de.ii.xtraplatform.features.domain.SchemaMapping; +import de.ii.xtraplatform.features.domain.SpatialExtent; +import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.gml.domain.ConnectionInfoWfsHttp; @@ -61,6 +64,7 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; import jakarta.ws.rs.core.MediaType; +import java.time.OffsetDateTime; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -311,10 +315,15 @@ public FeatureSchema getSortablesSchema( @Override public Optional getSpatialExtent(String typeName) { - if (getData().getTypes().containsKey(typeName)) { + if (!getData().getTypes().containsKey(typeName)) { return Optional.empty(); } + Optional configured = getConfiguredSpatialExtent(typeName); + if (configured.isPresent()) { + return configured; + } + try { Stream> extentGraph = aggregateStatsReader.getSpatialExtent( @@ -353,7 +362,80 @@ public Optional getSpatialExtent(String typeName, EpsgCrs crs) { @Override public Optional getTemporalExtent(String typeName) { - return Optional.empty(); + if (!getData().getTypes().containsKey(typeName)) { + return Optional.empty(); + } + + return getConfiguredTemporalExtent(typeName); + } + + private Optional getConfiguredSpatialExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getSpatial); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getSpatial); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getXmin() == null + || extent.getYmin() == null + || extent.getXmax() == null + || extent.getYmax() == null) { + return Optional.empty(); + } + EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); + if (extent.getZmin() != null && extent.getZmax() != null) { + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getZmin(), + extent.getXmax(), + extent.getYmax(), + extent.getZmax(), + nativeCrs)); + } + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getXmax(), + extent.getYmax(), + nativeCrs)); + }); + } + + private Optional getConfiguredTemporalExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getTemporal); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getTemporal); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getStart() == null && extent.getEnd() == null) { + return Optional.empty(); + } + OffsetDateTime start = + extent.getStart() != null + ? OffsetDateTime.parse(extent.getStart()) + : OffsetDateTime.parse("0001-01-01T00:00:00Z"); + OffsetDateTime end = + extent.getEnd() != null + ? OffsetDateTime.parse(extent.getEnd()) + : OffsetDateTime.parse("9999-12-31T23:59:59Z"); + return Optional.of(Interval.of(start.toInstant(), end.toInstant())); + }); } @Override diff --git a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java index 4158afa5f..48d850ec0 100644 --- a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java +++ b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java @@ -40,11 +40,14 @@ import de.ii.xtraplatform.features.domain.FeatureQueryEncoder; import de.ii.xtraplatform.features.domain.FeatureSchema; import de.ii.xtraplatform.features.domain.FeatureTokenDecoder; +import de.ii.xtraplatform.features.domain.FeatureTypeExtent; import de.ii.xtraplatform.features.domain.Metadata; import de.ii.xtraplatform.features.domain.ProviderData; import de.ii.xtraplatform.features.domain.ProviderExtensionRegistry; import de.ii.xtraplatform.features.domain.Query; import de.ii.xtraplatform.features.domain.SchemaMapping; +import de.ii.xtraplatform.features.domain.SpatialExtent; +import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.graphql.domain.FeatureProviderGraphQlData; @@ -52,6 +55,7 @@ import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; +import java.time.OffsetDateTime; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -307,7 +311,7 @@ public boolean is3dSupported() { @Override @SuppressWarnings("PMD.AvoidCatchingGenericException") public long getFeatureCount(String typeName) { - if (getData().getTypes().containsKey(typeName)) { + if (!getData().getTypes().containsKey(typeName)) { return -1; } @@ -353,10 +357,15 @@ public FeatureSchema getSortablesSchema( @Override @SuppressWarnings("PMD.AvoidCatchingGenericException") public Optional getSpatialExtent(String typeName) { - if (getData().getTypes().containsKey(typeName)) { + if (!getData().getTypes().containsKey(typeName)) { return Optional.empty(); } + Optional configured = getConfiguredSpatialExtent(typeName); + if (configured.isPresent()) { + return configured; + } + try { Stream> extentGraph = aggregateStatsReader.getSpatialExtent( @@ -394,7 +403,79 @@ public Optional getSpatialExtent(String typeName, EpsgCrs crs) { @Override public Optional getTemporalExtent(String typeName) { - return Optional.empty(); + if (!getData().getTypes().containsKey(typeName)) { + return Optional.empty(); + } + return getConfiguredTemporalExtent(typeName); + } + + private Optional getConfiguredSpatialExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getSpatial); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getSpatial); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getXmin() == null + || extent.getYmin() == null + || extent.getXmax() == null + || extent.getYmax() == null) { + return Optional.empty(); + } + EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); + if (extent.getZmin() != null && extent.getZmax() != null) { + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getZmin(), + extent.getXmax(), + extent.getYmax(), + extent.getZmax(), + nativeCrs)); + } + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getXmax(), + extent.getYmax(), + nativeCrs)); + }); + } + + private Optional getConfiguredTemporalExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getTemporal); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getTemporal); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getStart() == null && extent.getEnd() == null) { + return Optional.empty(); + } + OffsetDateTime start = + extent.getStart() != null + ? OffsetDateTime.parse(extent.getStart()) + : OffsetDateTime.parse("0001-01-01T00:00:00Z"); + OffsetDateTime end = + extent.getEnd() != null + ? OffsetDateTime.parse(extent.getEnd()) + : OffsetDateTime.parse("9999-12-31T23:59:59Z"); + return Optional.of(Interval.of(start.toInstant(), end.toInstant())); + }); } @Override diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java index 4274a15a0..2b3ed2a7f 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java @@ -78,6 +78,8 @@ import de.ii.xtraplatform.features.domain.SchemaMapping; import de.ii.xtraplatform.features.domain.SortKey; import de.ii.xtraplatform.features.domain.SourceSchemaValidator; +import de.ii.xtraplatform.features.domain.SpatialExtent; +import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.sql.ImmutableSqlPathSyntax; @@ -110,6 +112,7 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.streams.domain.Reactive.Transformer; import de.ii.xtraplatform.values.domain.ValueStore; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Collection; @@ -1026,6 +1029,11 @@ public Optional getSpatialExtent(String typeName) { return Optional.empty(); } + Optional configured = getConfiguredSpatialExtent(typeName); + if (configured.isPresent()) { + return configured; + } + String[] cacheKey = {typeName, "stats", "spatial"}; String cacheValidator = getData().getStableHash(); @@ -1101,6 +1109,11 @@ public Optional getTemporalExtent(String typeName) { return Optional.empty(); } + Optional configured = getConfiguredTemporalExtent(typeName); + if (configured.isPresent()) { + return configured; + } + String[] cacheKey = {typeName, "stats", "temporal"}; String cacheValidator = getData().getStableHash(); @@ -1180,6 +1193,79 @@ public Optional getTemporalExtent(String typeName) { return Optional.empty(); } + private Optional getConfiguredSpatialExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getSpatial); + Optional fromProvider = + getData() + .getExtent() + .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getSpatial); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getXmin() == null + || extent.getYmin() == null + || extent.getXmax() == null + || extent.getYmax() == null) { + return Optional.empty(); + } + EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); + if (extent.getZmin() != null && extent.getZmax() != null) { + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getZmin(), + extent.getXmax(), + extent.getYmax(), + extent.getZmax(), + nativeCrs)); + } + return Optional.of( + BoundingBox.of( + extent.getXmin(), + extent.getYmin(), + extent.getXmax(), + extent.getYmax(), + nativeCrs)); + }); + } + + private Optional getConfiguredTemporalExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getTemporal); + Optional fromProvider = + getData() + .getExtent() + .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getTemporal); + + return fromType + .or(() -> fromProvider) + .filter(extent -> extent.getComputed() == null) + .flatMap( + extent -> { + if (extent.getStart() == null && extent.getEnd() == null) { + return Optional.empty(); + } + OffsetDateTime start = + extent.getStart() != null + ? OffsetDateTime.parse(extent.getStart()) + : OffsetDateTime.parse("0001-01-01T00:00:00Z"); + OffsetDateTime end = + extent.getEnd() != null + ? OffsetDateTime.parse(extent.getEnd()) + : OffsetDateTime.parse("9999-12-31T23:59:59Z"); + return Optional.of(Interval.of(start.toInstant(), end.toInstant())); + }); + } + // TODO: cache deser does not work, because the the feature schemas have no name // (and there are no map keys to get them from) // so to implement this we need the split between cfg and internal schemas diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java index 85dc3024a..968b78c87 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureProviderDataV2.java @@ -204,7 +204,7 @@ default List getCql2Functions() { * @langDe Optionaler räumlicher und zeitlicher Extent für alle Types dieses Providers. Wenn * gesetzt, wird keine automatische Berechnung durchgeführt. */ - Optional getExtent(); + Optional getExtent(); // custom builder to automatically use keys of types as name of FeatureTypeV2 abstract class Builder> implements EntityDataBuilder { @@ -255,13 +255,5 @@ public T putFragments2(String key, ImmutableFeatureSchema.Builder builder) { @JsonProperty("extensions") public abstract T addAllExtensions(Iterable elements); - - /** - * @langEn Optional spatial and temporal extent for all types in this provider. If set, disables - * automatic calculation. - * @langDe Optionaler räumlicher und zeitlicher Extent für alle Types dieses Providers. Wenn - * gesetzt, wird keine automatische Berechnung durchgeführt. - */ - public abstract T extent(CollectionExtent extent); } } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java index 1fe3e1f4b..9e108cfc3 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureSchema.java @@ -222,12 +222,10 @@ default Type getType() { Optional getDescription(); /** - * @langEn Optional spatial and temporal extent for this feature type. If set, automatic - * calculation for this type can be skipped by consuming applications. - * @langDe Optionaler räumlicher und zeitlicher Extent für diesen Feature-Type. Wenn gesetzt, kann - * die automatische Berechnung für diesen Type in konsumierenden Anwendungen entfallen. + * @langEn Optional spatial and temporal extent for this feature type. + * @langDe Optionaler räumlicher und zeitlicher Extent für diesen Feature-Type. */ - Optional getExtent(); + Optional getExtent(); /** * @langEn The unit of measurement of the value, only relevant for numeric properties. diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java index 0ea035140..6d0c2f816 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeConfiguration.java @@ -42,5 +42,5 @@ public interface FeatureTypeConfiguration { * @langDe Optionaler räumlicher und zeitlicher Extent für diesen Type. Wenn gesetzt, wird keine * automatische Berechnung für diesen Type durchgeführt. */ - Optional getExtent(); + Optional getExtent(); } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeExtent.java similarity index 74% rename from xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java rename to xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeExtent.java index cc0d7d7e6..bfe1a1110 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/CollectionExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/FeatureTypeExtent.java @@ -8,15 +8,14 @@ package de.ii.xtraplatform.features.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import de.ii.xtraplatform.crs.domain.BoundingBox; import java.util.Optional; import org.immutables.value.Value; /** Extent object for spatial and temporal extents. */ @Value.Immutable -@JsonDeserialize(builder = ImmutableCollectionExtent.Builder.class) -public interface CollectionExtent { - Optional getSpatial(); +@JsonDeserialize(builder = ImmutableFeatureTypeExtent.Builder.class) +public interface FeatureTypeExtent { + Optional getSpatial(); Optional getTemporal(); } diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java new file mode 100644 index 000000000..2623b8081 --- /dev/null +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 interactive instruments GmbH + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package de.ii.xtraplatform.features.domain; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Preconditions; +import javax.annotation.Nullable; +import org.immutables.value.Value; + +/** Spatial extent in native CRS. */ +@Value.Immutable +@JsonDeserialize(builder = ImmutableSpatialExtent.Builder.class) +public interface SpatialExtent { + + @Nullable + Double getXmin(); + + @Nullable + Double getYmin(); + + @Nullable + Double getZmin(); + + @Nullable + Double getXmax(); + + @Nullable + Double getYmax(); + + @Nullable + Double getZmax(); + + @Nullable + Boolean getComputed(); + + @Value.Check + default void checkExclusiveComputed() { + boolean hasCoordinates = + getXmin() != null + || getYmin() != null + || getZmin() != null + || getXmax() != null + || getYmax() != null + || getZmax() != null; + boolean hasComputed = getComputed() != null; + + Preconditions.checkState( + !(hasCoordinates && hasComputed), + "SpatialExtent: 'computed' and explicit coordinates must not be set at the same time."); + + if (hasCoordinates) { + Preconditions.checkState( + getXmin() != null && getYmin() != null && getXmax() != null && getYmax() != null, + "SpatialExtent: xmin, ymin, xmax, ymax are required when coordinates are used."); + + Preconditions.checkState( + (getZmin() == null && getZmax() == null) || (getZmin() != null && getZmax() != null), + "SpatialExtent: zmin and zmax must both be set or both be absent."); + } + } +} diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java index 87b237ccb..b595487f3 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java @@ -8,21 +8,62 @@ package de.ii.xtraplatform.features.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Preconditions; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import javax.annotation.Nullable; import org.immutables.value.Value; -/** Temporal extent with start and end as Unix timestamp in milliseconds. */ +/** Temporal extent with ISO-8601 dates (yyyy-MM-dd). */ @Value.Immutable @JsonDeserialize(builder = ImmutableTemporalExtent.Builder.class) public interface TemporalExtent { - /** - * Start of the temporal extent as Unix timestamp in milliseconds, or {@code null} for open start. - */ + /** Start of the temporal extent as ISO-8601 date (yyyy-MM-dd), or {@code null} for open start. */ @Nullable - Long getStart(); + String getStart(); - /** End of the temporal extent as Unix timestamp in milliseconds, or {@code null} for open end. */ + /** End of the temporal extent as ISO-8601 date (yyyy-MM-dd), or {@code null} for open end. */ @Nullable - Long getEnd(); + String getEnd(); + + @Nullable + Boolean getComputed(); + + @Value.Check + default void checkExclusiveComputed() { + boolean hasBounds = getStart() != null || getEnd() != null; + boolean hasComputed = getComputed() != null; + + Preconditions.checkState( + !(hasBounds && hasComputed), + "TemporalExtent: 'computed' and explicit start/end must not be set at the same time."); + + if (getStart() != null) { + validateIsoDate(getStart(), "start"); + } + if (getEnd() != null) { + validateIsoDate(getEnd(), "end"); + } + } + + private static void validateIsoDate(String value, String field) { + Preconditions.checkState( + !value.matches("^-?\\d+$"), + "TemporalExtent: '%s' must be an ISO date string (yyyy-MM-dd), not a numeric timestamp.", + field); + Preconditions.checkState( + !value.contains("T"), + "TemporalExtent: '%s' must be an ISO date string (yyyy-MM-dd), timestamps are not allowed.", + field); + try { + LocalDate.parse(value); + } catch (DateTimeParseException e) { + Preconditions.checkState( + false, + "TemporalExtent: '%s' is not a valid ISO-8601 date (yyyy-MM-dd): %s", + field, + value); + } + } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java index b103f0f62..b1accc13a 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderFeatures.java @@ -26,6 +26,8 @@ import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.crs.domain.CrsInfo; import de.ii.xtraplatform.crs.domain.CrsTransformerFactory; +import de.ii.xtraplatform.crs.domain.EpsgCrs; +import de.ii.xtraplatform.crs.domain.OgcCrs; import de.ii.xtraplatform.entities.domain.Entity; import de.ii.xtraplatform.entities.domain.Entity.SubType; import de.ii.xtraplatform.entities.domain.EntityRegistry; @@ -37,6 +39,7 @@ import de.ii.xtraplatform.features.domain.FeatureProvider.FeatureVolatileCapability; import de.ii.xtraplatform.features.domain.FeatureSchema; import de.ii.xtraplatform.features.domain.ProviderData; +import de.ii.xtraplatform.features.domain.SpatialExtent; import de.ii.xtraplatform.jobs.domain.JobQueue; import de.ii.xtraplatform.tiles.domain.Cache; import de.ii.xtraplatform.tiles.domain.Cache.Storage; @@ -1000,11 +1003,9 @@ private TilesetMetadata loadMetadata(TilesetFeatures tileset) { .map(id -> tileGenerator.getVectorSchema(id, FeatureEncoderMVT.FORMAT)) .collect(Collectors.toList()); - boolean forceCompute = getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); Optional configuredExtent = - forceCompute - ? Optional.empty() - : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + toBoundingBox( + tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()), tileset); Optional bounds; if (configuredExtent.isPresent()) { @@ -1136,4 +1137,40 @@ private List getFeatureProviders() { private String getFeatureProviderId(TilesetFeatures tileset) { return tileset.getFeatureProvider().orElse(TileProviderFeatures.clean(getData().getId())); } + + private Optional toBoundingBox( + Optional extent, TilesetFeatures tileset) { + EpsgCrs nativeCrs = resolveNativeCrs(tileset); + return extent + .filter(e -> e.getComputed() == null) + .flatMap( + e -> { + if (e.getXmin() == null + || e.getYmin() == null + || e.getXmax() == null + || e.getYmax() == null) { + return Optional.empty(); + } + if (e.getZmin() != null && e.getZmax() != null) { + return Optional.of( + BoundingBox.of( + e.getXmin(), + e.getYmin(), + e.getZmin(), + e.getXmax(), + e.getYmax(), + e.getZmax(), + nativeCrs)); + } + return Optional.of( + BoundingBox.of(e.getXmin(), e.getYmin(), e.getXmax(), e.getYmax(), nativeCrs)); + }); + } + + private EpsgCrs resolveNativeCrs(TilesetFeatures tileset) { + return tileGenerator + .getFeatureProvider(getFeatureProviderId(tileset)) + .map(provider -> provider.crs().get().getNativeCrs()) + .orElse(OgcCrs.CRS84); + } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java index 3cf3926e6..6daa17789 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderHttp.java @@ -12,9 +12,11 @@ import dagger.assisted.AssistedInject; import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry; import de.ii.xtraplatform.crs.domain.BoundingBox; +import de.ii.xtraplatform.crs.domain.OgcCrs; import de.ii.xtraplatform.entities.domain.Entity; import de.ii.xtraplatform.entities.domain.Entity.SubType; import de.ii.xtraplatform.features.domain.ProviderData; +import de.ii.xtraplatform.features.domain.SpatialExtent; import de.ii.xtraplatform.tiles.domain.ChainedTileProvider; import de.ii.xtraplatform.tiles.domain.ImmutableTilesetMetadata; import de.ii.xtraplatform.tiles.domain.TileAccess; @@ -118,11 +120,8 @@ private void loadMetadata() { } private TilesetMetadata loadMetadata(TilesetHttp tileset) { - boolean forceCompute = getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); Optional bounds = - forceCompute - ? Optional.empty() - : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + toBoundingBox(tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent())); return ImmutableTilesetMetadata.builder() .encodings( @@ -137,4 +136,31 @@ private TilesetMetadata loadMetadata(TilesetHttp tileset) { .bounds(bounds) .build(); } + + private Optional toBoundingBox(Optional extent) { + return extent + .filter(e -> e.getComputed() == null) + .flatMap( + e -> { + if (e.getXmin() == null + || e.getYmin() == null + || e.getXmax() == null + || e.getYmax() == null) { + return Optional.empty(); + } + if (e.getZmin() != null && e.getZmax() != null) { + return Optional.of( + BoundingBox.of( + e.getXmin(), + e.getYmin(), + e.getZmin(), + e.getXmax(), + e.getYmax(), + e.getZmax(), + OgcCrs.CRS84)); + } + return Optional.of( + BoundingBox.of(e.getXmin(), e.getYmin(), e.getXmax(), e.getYmax(), OgcCrs.CRS84)); + }); + } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java index 5b6a54900..a9c278e9d 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/app/TileProviderMbTiles.java @@ -20,6 +20,7 @@ import de.ii.xtraplatform.entities.domain.Entity.SubType; import de.ii.xtraplatform.features.domain.FeatureSchema; import de.ii.xtraplatform.features.domain.ProviderData; +import de.ii.xtraplatform.features.domain.SpatialExtent; import de.ii.xtraplatform.tiles.domain.ChainedTileProvider; import de.ii.xtraplatform.tiles.domain.ImmutableMinMax; import de.ii.xtraplatform.tiles.domain.ImmutableTilesetMetadata; @@ -182,12 +183,9 @@ private void loadMetadata(Map tilesetSources) { .getTilesets() .get(tilesetKey.first()) .mergeDefaults(getData().getTilesetDefaults()); - boolean forceCompute = - getData().getTilesetDefaults().getSpatialExtentComputed().orElse(false); Optional configuredExtent = - forceCompute - ? Optional.empty() - : tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent()); + toBoundingBox( + tileset.getExtent().or(() -> getData().getTilesetDefaults().getExtent())); metadata.put( tilesetKey.first(), loadMetadata(tilesetKey.second(), path, configuredExtent)); @@ -258,4 +256,31 @@ private Tuple toTuple(String tilesetKey) { String[] split = tilesetKey.split("/"); return Tuple.of(split[0], split[1]); } + + private Optional toBoundingBox(Optional extent) { + return extent + .filter(e -> e.getComputed() == null) + .flatMap( + e -> { + if (e.getXmin() == null + || e.getYmin() == null + || e.getXmax() == null + || e.getYmax() == null) { + return Optional.empty(); + } + if (e.getZmin() != null && e.getZmax() != null) { + return Optional.of( + BoundingBox.of( + e.getXmin(), + e.getYmin(), + e.getZmin(), + e.getXmax(), + e.getYmax(), + e.getZmax(), + OgcCrs.CRS84)); + } + return Optional.of( + BoundingBox.of(e.getXmin(), e.getYmin(), e.getXmax(), e.getYmax(), OgcCrs.CRS84)); + }); + } } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java index 107ab7104..49ca3bb45 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetCommon.java @@ -7,8 +7,8 @@ */ package de.ii.xtraplatform.tiles.domain; -import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import de.ii.xtraplatform.features.domain.SpatialExtent; import de.ii.xtraplatform.tiles.domain.ImmutableMinMax.Builder; import java.util.Optional; @@ -27,10 +27,8 @@ public interface TilesetCommon extends TilesetCommonDefaults { Optional getCenter(); /** - * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip - * bounding box. - * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert - * als Clip-BoundingBox verwendet. + * @langEn Optional fixed spatial extent for this tileset in native CRS. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset im nativen CRS. */ - Optional getExtent(); + Optional getExtent(); } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java index 67e574ac4..2941a408f 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetFeaturesDefaults.java @@ -8,10 +8,10 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.Buildable; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableBuilder; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import de.ii.xtraplatform.features.domain.SpatialExtent; import java.util.Optional; import org.immutables.value.Value; @@ -39,22 +39,8 @@ default ImmutableTilesetFeaturesDefaults.Builder getBuilder() { abstract class Builder implements BuildableBuilder {} /** - * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip - * bounding box instead of computing it. - * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert - * als Clip-BoundingBox verwendet, statt ihn zu berechnen. + * @langEn Optional fixed spatial extent for this tileset in native CRS. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset im nativen CRS. */ - Optional getExtent(); - - /** - * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is - * set globally. If false, a fixed value is always used. If not set, the global or provider - * logic applies. - * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global - * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht - * gesetzt, gilt die globale oder Provider-Logik. - */ - default Optional getSpatialExtentComputed() { - return Optional.empty(); - } + Optional getExtent(); } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java index 93da07a24..9c9250e94 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetHttpDefaults.java @@ -8,10 +8,10 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.entities.domain.maptobuilder.Buildable; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableBuilder; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import de.ii.xtraplatform.features.domain.SpatialExtent; import java.util.Optional; import org.immutables.value.Value; @@ -34,24 +34,10 @@ default ImmutableTilesetHttpDefaults.Builder getBuilder() { } /** - * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip - * bounding box instead of falling back to TileMatrixSet bounds. - * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert - * als Clip-BoundingBox verwendet, statt auf TileMatrixSet-Bounds zurückzufallen. + * @langEn Optional fixed spatial extent for this tileset in native CRS. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset im nativen CRS. */ - Optional getExtent(); - - /** - * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is - * set globally. If false, a fixed value is always used. If not set, the global or provider - * logic applies. - * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global - * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht - * gesetzt, gilt die globale oder Provider-Logik. - */ - default Optional getSpatialExtentComputed() { - return Optional.empty(); - } + Optional getExtent(); abstract class Builder implements BuildableBuilder {} } diff --git a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java index 8f51b6fa6..1ccdcff11 100644 --- a/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java +++ b/xtraplatform-tiles/src/main/java/de/ii/xtraplatform/tiles/domain/TilesetMbTilesDefaults.java @@ -8,9 +8,9 @@ package de.ii.xtraplatform.tiles.domain; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.docs.DocIgnore; import de.ii.xtraplatform.entities.domain.maptobuilder.BuildableMap; +import de.ii.xtraplatform.features.domain.SpatialExtent; import de.ii.xtraplatform.tiles.domain.ImmutableMinMax.Builder; import java.util.Optional; import org.immutables.value.Value; @@ -39,22 +39,8 @@ default String getTileMatrixSet() { } /** - * @langEn Optional fixed spatial extent for this tileset. If set, this value is used as the clip - * bounding box instead of falling back to MBTiles metadata. - * @langDe Optionaler fester räumlicher Extent für dieses Tileset. Wenn gesetzt, wird dieser Wert - * als Clip-BoundingBox verwendet, statt auf MBTiles-Metadaten zurückzufallen. + * @langEn Optional fixed spatial extent for this tileset in native CRS. + * @langDe Optionaler fester räumlicher Extent für dieses Tileset im nativen CRS. */ - Optional getExtent(); - - /** - * @langEn If true, the spatial extent will always be computed from data, even if a fixed value is - * set globally. If false, a fixed value is always used. If not set, the global or provider - * logic applies. - * @langDe Wenn true, wird der räumliche Extent immer aus den Daten berechnet, auch wenn global - * ein fester Wert gesetzt ist. Wenn false, wird immer ein fixer Wert verwendet. Wenn nicht - * gesetzt, gilt die globale oder Provider-Logik. - */ - default Optional getSpatialExtentComputed() { - return Optional.empty(); - } + Optional getExtent(); } From 0b340c96f78c3058d97458a84a84c9faa034d89c Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Thu, 21 May 2026 18:31:53 +0200 Subject: [PATCH 4/7] treat computed=true only; handle date-only bounds --- .../features/gml/app/FeatureProviderWfs.java | 20 +++++++++++++++---- .../graphql/app/FeatureProviderGraphQl.java | 20 +++++++++++++++---- .../sql/domain/FeatureProviderSql.java | 20 +++++++++++++++---- .../features/domain/SpatialExtent.java | 4 ++-- .../features/domain/TemporalExtent.java | 4 ++-- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java index aaaa22fc8..e2b9321f3 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java @@ -64,7 +64,9 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; import jakarta.ws.rs.core.MediaType; +import java.time.LocalDate; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -379,7 +381,7 @@ private Optional getConfiguredSpatialExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getXmin() == null @@ -420,7 +422,7 @@ private Optional getConfiguredTemporalExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getStart() == null && extent.getEnd() == null) { @@ -428,16 +430,26 @@ private Optional getConfiguredTemporalExtent(String typeName) { } OffsetDateTime start = extent.getStart() != null - ? OffsetDateTime.parse(extent.getStart()) + ? parseConfiguredTemporalBound(extent.getStart(), false) : OffsetDateTime.parse("0001-01-01T00:00:00Z"); OffsetDateTime end = extent.getEnd() != null - ? OffsetDateTime.parse(extent.getEnd()) + ? parseConfiguredTemporalBound(extent.getEnd(), true) : OffsetDateTime.parse("9999-12-31T23:59:59Z"); return Optional.of(Interval.of(start.toInstant(), end.toInstant())); }); } + private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { + if (value.contains("T")) { + return OffsetDateTime.parse(value); + } + + return endOfDay + ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) + : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); + } + @Override public Optional getMetadata() { return getConnector().getMetadata(); diff --git a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java index 48d850ec0..2691f1b27 100644 --- a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java +++ b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java @@ -55,7 +55,9 @@ import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; +import java.time.LocalDate; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -419,7 +421,7 @@ private Optional getConfiguredSpatialExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getXmin() == null @@ -460,7 +462,7 @@ private Optional getConfiguredTemporalExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getStart() == null && extent.getEnd() == null) { @@ -468,16 +470,26 @@ private Optional getConfiguredTemporalExtent(String typeName) { } OffsetDateTime start = extent.getStart() != null - ? OffsetDateTime.parse(extent.getStart()) + ? parseConfiguredTemporalBound(extent.getStart(), false) : OffsetDateTime.parse("0001-01-01T00:00:00Z"); OffsetDateTime end = extent.getEnd() != null - ? OffsetDateTime.parse(extent.getEnd()) + ? parseConfiguredTemporalBound(extent.getEnd(), true) : OffsetDateTime.parse("9999-12-31T23:59:59Z"); return Optional.of(Interval.of(start.toInstant(), end.toInstant())); }); } + private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { + if (value.contains("T")) { + return OffsetDateTime.parse(value); + } + + return endOfDay + ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) + : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); + } + @Override public Optional getMetadata() { return Optional.empty(); diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java index 2b3ed2a7f..e23abf44b 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java @@ -112,8 +112,10 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.streams.domain.Reactive.Transformer; import de.ii.xtraplatform.values.domain.ValueStore; +import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Collection; import java.util.LinkedHashMap; @@ -1205,7 +1207,7 @@ private Optional getConfiguredSpatialExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getXmin() == null @@ -1248,7 +1250,7 @@ private Optional getConfiguredTemporalExtent(String typeName) { return fromType .or(() -> fromProvider) - .filter(extent -> extent.getComputed() == null) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) .flatMap( extent -> { if (extent.getStart() == null && extent.getEnd() == null) { @@ -1256,16 +1258,26 @@ private Optional getConfiguredTemporalExtent(String typeName) { } OffsetDateTime start = extent.getStart() != null - ? OffsetDateTime.parse(extent.getStart()) + ? parseConfiguredTemporalBound(extent.getStart(), false) : OffsetDateTime.parse("0001-01-01T00:00:00Z"); OffsetDateTime end = extent.getEnd() != null - ? OffsetDateTime.parse(extent.getEnd()) + ? parseConfiguredTemporalBound(extent.getEnd(), true) : OffsetDateTime.parse("9999-12-31T23:59:59Z"); return Optional.of(Interval.of(start.toInstant(), end.toInstant())); }); } + private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { + if (value.contains("T")) { + return OffsetDateTime.parse(value); + } + + return endOfDay + ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) + : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); + } + // TODO: cache deser does not work, because the the feature schemas have no name // (and there are no map keys to get them from) // so to implement this we need the split between cfg and internal schemas diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java index 2623b8081..c771d3318 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java @@ -47,10 +47,10 @@ default void checkExclusiveComputed() { || getXmax() != null || getYmax() != null || getZmax() != null; - boolean hasComputed = getComputed() != null; + boolean autoCompute = Boolean.TRUE.equals(getComputed()); Preconditions.checkState( - !(hasCoordinates && hasComputed), + !(hasCoordinates && autoCompute), "SpatialExtent: 'computed' and explicit coordinates must not be set at the same time."); if (hasCoordinates) { diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java index b595487f3..048d018e1 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java @@ -33,10 +33,10 @@ public interface TemporalExtent { @Value.Check default void checkExclusiveComputed() { boolean hasBounds = getStart() != null || getEnd() != null; - boolean hasComputed = getComputed() != null; + boolean autoCompute = Boolean.TRUE.equals(getComputed()); Preconditions.checkState( - !(hasBounds && hasComputed), + !(hasBounds && autoCompute), "TemporalExtent: 'computed' and explicit start/end must not be set at the same time."); if (getStart() != null) { From 1ac677f7fb8e8171ddcae165221904fcf4483030 Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Fri, 22 May 2026 12:23:48 +0200 Subject: [PATCH 5/7] adjustments temporalExtent --- .../features/domain/TemporalExtent.java | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java index 048d018e1..7fa599d5e 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/TemporalExtent.java @@ -7,29 +7,51 @@ */ package de.ii.xtraplatform.features.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.Preconditions; -import java.time.LocalDate; +import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import javax.annotation.Nullable; import org.immutables.value.Value; -/** Temporal extent with ISO-8601 dates (yyyy-MM-dd). */ +/** Temporal extent with UTC ISO-8601 instants (yyyy-MM-ddTHH:mm:ss.SSSZ). */ @Value.Immutable @JsonDeserialize(builder = ImmutableTemporalExtent.Builder.class) public interface TemporalExtent { - /** Start of the temporal extent as ISO-8601 date (yyyy-MM-dd), or {@code null} for open start. */ + /** + * Start of the temporal extent as UTC ISO-8601 instant (yyyy-MM-ddTHH:mm:ss[.SSS]Z), or {@code + * null} for open start. + */ @Nullable String getStart(); - /** End of the temporal extent as ISO-8601 date (yyyy-MM-dd), or {@code null} for open end. */ + /** + * End of the temporal extent as UTC ISO-8601 instant (yyyy-MM-ddTHH:mm:ss[.SSS]Z), or {@code + * null} for open end. + */ @Nullable String getEnd(); @Nullable Boolean getComputed(); + @Value.Derived + @JsonIgnore + default Instant getStartInstant() { + return getStart() == null + ? null + : Instant.from(DateTimeFormatter.ISO_INSTANT.parse(getStart())); + } + + @Value.Derived + @JsonIgnore + default Instant getEndInstant() { + return getEnd() == null ? null : Instant.from(DateTimeFormatter.ISO_INSTANT.parse(getEnd())); + } + @Value.Check default void checkExclusiveComputed() { boolean hasBounds = getStart() != null || getEnd() != null; @@ -40,28 +62,24 @@ default void checkExclusiveComputed() { "TemporalExtent: 'computed' and explicit start/end must not be set at the same time."); if (getStart() != null) { - validateIsoDate(getStart(), "start"); + validateIsoInstant(getStart(), "start"); } if (getEnd() != null) { - validateIsoDate(getEnd(), "end"); + validateIsoInstant(getEnd(), "end"); } } - private static void validateIsoDate(String value, String field) { + private static void validateIsoInstant(String value, String field) { Preconditions.checkState( !value.matches("^-?\\d+$"), - "TemporalExtent: '%s' must be an ISO date string (yyyy-MM-dd), not a numeric timestamp.", - field); - Preconditions.checkState( - !value.contains("T"), - "TemporalExtent: '%s' must be an ISO date string (yyyy-MM-dd), timestamps are not allowed.", + "TemporalExtent: '%s' must be a UTC ISO-8601 instant string, not a numeric timestamp.", field); try { - LocalDate.parse(value); + DateTimeFormatter.ISO_INSTANT.parse(value); } catch (DateTimeParseException e) { Preconditions.checkState( false, - "TemporalExtent: '%s' is not a valid ISO-8601 date (yyyy-MM-dd): %s", + "TemporalExtent: '%s' is not a valid UTC ISO-8601 instant (yyyy-MM-ddTHH:mm:ss.SSSZ): %s", field, value); } From 6618e4d39b375e1a7b72cadd43098a50087f67bd Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Fri, 22 May 2026 13:20:34 +0200 Subject: [PATCH 6/7] Update SpatialExtent.java --- .../features/domain/SpatialExtent.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java index c771d3318..23ae71f1e 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/SpatialExtent.java @@ -9,6 +9,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.Preconditions; +import de.ii.xtraplatform.crs.domain.BoundingBox; +import de.ii.xtraplatform.crs.domain.EpsgCrs; +import java.util.Optional; import javax.annotation.Nullable; import org.immutables.value.Value; @@ -63,4 +66,18 @@ default void checkExclusiveComputed() { "SpatialExtent: zmin and zmax must both be set or both be absent."); } } + + default Optional toBoundingBox(EpsgCrs nativeCrs) { + if (getXmin() == null || getYmin() == null || getXmax() == null || getYmax() == null) { + return Optional.empty(); + } + + if (getZmin() != null && getZmax() != null) { + return Optional.of( + BoundingBox.of( + getXmin(), getYmin(), getZmin(), getXmax(), getYmax(), getZmax(), nativeCrs)); + } + + return Optional.of(BoundingBox.of(getXmin(), getYmin(), getXmax(), getYmax(), nativeCrs)); + } } From a6811e3b43da1152f8a504e61027be4eb9c6f35f Mon Sep 17 00:00:00 2001 From: "p.zahnen" Date: Fri, 22 May 2026 14:19:42 +0200 Subject: [PATCH 7/7] refactoring --- .../features/gml/app/FeatureProviderWfs.java | 99 +---------------- .../graphql/app/FeatureProviderGraphQl.java | 99 +---------------- .../sql/domain/FeatureProviderSql.java | 101 +----------------- .../domain/AbstractFeatureProvider.java | 72 +++++++++++++ 4 files changed, 75 insertions(+), 296 deletions(-) diff --git a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java index e2b9321f3..0c642dd6c 100644 --- a/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java +++ b/xtraplatform-features-gml/src/main/java/de/ii/xtraplatform/features/gml/app/FeatureProviderWfs.java @@ -15,7 +15,6 @@ import de.ii.xtraplatform.cql.domain.Cql; import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.crs.domain.CrsInfo; -import de.ii.xtraplatform.crs.domain.CrsTransformationException; import de.ii.xtraplatform.crs.domain.CrsTransformerFactory; import de.ii.xtraplatform.crs.domain.EpsgCrs; import de.ii.xtraplatform.crs.domain.OgcCrs; @@ -46,14 +45,11 @@ import de.ii.xtraplatform.features.domain.FeatureStream; import de.ii.xtraplatform.features.domain.FeatureStreamImpl; import de.ii.xtraplatform.features.domain.FeatureTokenDecoder; -import de.ii.xtraplatform.features.domain.FeatureTypeExtent; import de.ii.xtraplatform.features.domain.Metadata; import de.ii.xtraplatform.features.domain.ProviderData; import de.ii.xtraplatform.features.domain.ProviderExtensionRegistry; import de.ii.xtraplatform.features.domain.Query; import de.ii.xtraplatform.features.domain.SchemaMapping; -import de.ii.xtraplatform.features.domain.SpatialExtent; -import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.gml.domain.ConnectionInfoWfsHttp; @@ -64,9 +60,6 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; import jakarta.ws.rs.core.MediaType; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -348,18 +341,7 @@ public Optional getSpatialExtent(String typeName) { @Override public Optional getSpatialExtent(String typeName, EpsgCrs crs) { return getSpatialExtent(typeName) - .flatMap( - boundingBox -> - crsTransformerFactory - .getTransformer(getNativeCrs(), crs, false) - .flatMap( - crsTransformer -> { - try { - return Optional.of(crsTransformer.transformBoundingBox(boundingBox)); - } catch (CrsTransformationException e) { - return Optional.empty(); - } - })); + .flatMap(boundingBox -> transformSpatialExtent(boundingBox, crs)); } @Override @@ -371,85 +353,6 @@ public Optional getTemporalExtent(String typeName) { return getConfiguredTemporalExtent(typeName); } - private Optional getConfiguredSpatialExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(FeatureTypeExtent::getSpatial); - Optional fromProvider = - getData().getExtent().flatMap(FeatureTypeExtent::getSpatial); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getXmin() == null - || extent.getYmin() == null - || extent.getXmax() == null - || extent.getYmax() == null) { - return Optional.empty(); - } - EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); - if (extent.getZmin() != null && extent.getZmax() != null) { - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getZmin(), - extent.getXmax(), - extent.getYmax(), - extent.getZmax(), - nativeCrs)); - } - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getXmax(), - extent.getYmax(), - nativeCrs)); - }); - } - - private Optional getConfiguredTemporalExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(FeatureTypeExtent::getTemporal); - Optional fromProvider = - getData().getExtent().flatMap(FeatureTypeExtent::getTemporal); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getStart() == null && extent.getEnd() == null) { - return Optional.empty(); - } - OffsetDateTime start = - extent.getStart() != null - ? parseConfiguredTemporalBound(extent.getStart(), false) - : OffsetDateTime.parse("0001-01-01T00:00:00Z"); - OffsetDateTime end = - extent.getEnd() != null - ? parseConfiguredTemporalBound(extent.getEnd(), true) - : OffsetDateTime.parse("9999-12-31T23:59:59Z"); - return Optional.of(Interval.of(start.toInstant(), end.toInstant())); - }); - } - - private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { - if (value.contains("T")) { - return OffsetDateTime.parse(value); - } - - return endOfDay - ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) - : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); - } - @Override public Optional getMetadata() { return getConnector().getMetadata(); diff --git a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java index 2691f1b27..a880989ac 100644 --- a/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java +++ b/xtraplatform-features-graphql/src/main/java/de/ii/xtraplatform/features/graphql/app/FeatureProviderGraphQl.java @@ -14,7 +14,6 @@ import de.ii.xtraplatform.cql.domain.Cql; import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.crs.domain.CrsInfo; -import de.ii.xtraplatform.crs.domain.CrsTransformationException; import de.ii.xtraplatform.crs.domain.CrsTransformerFactory; import de.ii.xtraplatform.crs.domain.EpsgCrs; import de.ii.xtraplatform.crs.domain.OgcCrs; @@ -40,14 +39,11 @@ import de.ii.xtraplatform.features.domain.FeatureQueryEncoder; import de.ii.xtraplatform.features.domain.FeatureSchema; import de.ii.xtraplatform.features.domain.FeatureTokenDecoder; -import de.ii.xtraplatform.features.domain.FeatureTypeExtent; import de.ii.xtraplatform.features.domain.Metadata; import de.ii.xtraplatform.features.domain.ProviderData; import de.ii.xtraplatform.features.domain.ProviderExtensionRegistry; import de.ii.xtraplatform.features.domain.Query; import de.ii.xtraplatform.features.domain.SchemaMapping; -import de.ii.xtraplatform.features.domain.SpatialExtent; -import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.graphql.domain.FeatureProviderGraphQlData; @@ -55,9 +51,6 @@ import de.ii.xtraplatform.streams.domain.Reactive; import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.ValueStore; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; @@ -389,18 +382,7 @@ public Optional getSpatialExtent(String typeName) { @Override public Optional getSpatialExtent(String typeName, EpsgCrs crs) { return getSpatialExtent(typeName) - .flatMap( - boundingBox -> - crsTransformerFactory - .getTransformer(getNativeCrs(), crs, false) - .flatMap( - crsTransformer -> { - try { - return Optional.of(crsTransformer.transformBoundingBox(boundingBox)); - } catch (CrsTransformationException e) { - return Optional.empty(); - } - })); + .flatMap(boundingBox -> transformSpatialExtent(boundingBox, crs)); } @Override @@ -411,85 +393,6 @@ public Optional getTemporalExtent(String typeName) { return getConfiguredTemporalExtent(typeName); } - private Optional getConfiguredSpatialExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(FeatureTypeExtent::getSpatial); - Optional fromProvider = - getData().getExtent().flatMap(FeatureTypeExtent::getSpatial); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getXmin() == null - || extent.getYmin() == null - || extent.getXmax() == null - || extent.getYmax() == null) { - return Optional.empty(); - } - EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); - if (extent.getZmin() != null && extent.getZmax() != null) { - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getZmin(), - extent.getXmax(), - extent.getYmax(), - extent.getZmax(), - nativeCrs)); - } - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getXmax(), - extent.getYmax(), - nativeCrs)); - }); - } - - private Optional getConfiguredTemporalExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(FeatureTypeExtent::getTemporal); - Optional fromProvider = - getData().getExtent().flatMap(FeatureTypeExtent::getTemporal); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getStart() == null && extent.getEnd() == null) { - return Optional.empty(); - } - OffsetDateTime start = - extent.getStart() != null - ? parseConfiguredTemporalBound(extent.getStart(), false) - : OffsetDateTime.parse("0001-01-01T00:00:00Z"); - OffsetDateTime end = - extent.getEnd() != null - ? parseConfiguredTemporalBound(extent.getEnd(), true) - : OffsetDateTime.parse("9999-12-31T23:59:59Z"); - return Optional.of(Interval.of(start.toInstant(), end.toInstant())); - }); - } - - private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { - if (value.contains("T")) { - return OffsetDateTime.parse(value); - } - - return endOfDay - ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) - : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); - } - @Override public Optional getMetadata() { return Optional.empty(); diff --git a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java index e23abf44b..1f1cd230e 100644 --- a/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java +++ b/xtraplatform-features-sql/src/main/java/de/ii/xtraplatform/features/sql/domain/FeatureProviderSql.java @@ -78,8 +78,6 @@ import de.ii.xtraplatform.features.domain.SchemaMapping; import de.ii.xtraplatform.features.domain.SortKey; import de.ii.xtraplatform.features.domain.SourceSchemaValidator; -import de.ii.xtraplatform.features.domain.SpatialExtent; -import de.ii.xtraplatform.features.domain.TemporalExtent; import de.ii.xtraplatform.features.domain.transform.OnlyQueryables; import de.ii.xtraplatform.features.domain.transform.OnlySortables; import de.ii.xtraplatform.features.sql.ImmutableSqlPathSyntax; @@ -112,10 +110,7 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.streams.domain.Reactive.Transformer; import de.ii.xtraplatform.values.domain.ValueStore; -import java.time.LocalDate; -import java.time.OffsetDateTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Collection; import java.util.LinkedHashMap; @@ -1091,18 +1086,7 @@ public Optional getSpatialExtent(String typeName) { @Override public Optional getSpatialExtent(String typeName, EpsgCrs crs) { return getSpatialExtent(typeName) - .flatMap( - boundingBox -> - crsTransformerFactory - .getTransformer(getNativeCrs(), crs, false) - .flatMap( - crsTransformer -> { - try { - return Optional.of(crsTransformer.transformBoundingBox(boundingBox)); - } catch (Exception e) { - return Optional.empty(); - } - })); + .flatMap(boundingBox -> transformSpatialExtent(boundingBox, crs)); } @Override @@ -1195,89 +1179,6 @@ public Optional getTemporalExtent(String typeName) { return Optional.empty(); } - private Optional getConfiguredSpatialExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getSpatial); - Optional fromProvider = - getData() - .getExtent() - .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getSpatial); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getXmin() == null - || extent.getYmin() == null - || extent.getXmax() == null - || extent.getYmax() == null) { - return Optional.empty(); - } - EpsgCrs nativeCrs = getData().getNativeCrs().orElse(OgcCrs.CRS84); - if (extent.getZmin() != null && extent.getZmax() != null) { - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getZmin(), - extent.getXmax(), - extent.getYmax(), - extent.getZmax(), - nativeCrs)); - } - return Optional.of( - BoundingBox.of( - extent.getXmin(), - extent.getYmin(), - extent.getXmax(), - extent.getYmax(), - nativeCrs)); - }); - } - - private Optional getConfiguredTemporalExtent(String typeName) { - Optional fromType = - Optional.ofNullable(getData().getTypes().get(typeName)) - .flatMap(FeatureSchema::getExtent) - .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getTemporal); - Optional fromProvider = - getData() - .getExtent() - .flatMap(de.ii.xtraplatform.features.domain.FeatureTypeExtent::getTemporal); - - return fromType - .or(() -> fromProvider) - .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) - .flatMap( - extent -> { - if (extent.getStart() == null && extent.getEnd() == null) { - return Optional.empty(); - } - OffsetDateTime start = - extent.getStart() != null - ? parseConfiguredTemporalBound(extent.getStart(), false) - : OffsetDateTime.parse("0001-01-01T00:00:00Z"); - OffsetDateTime end = - extent.getEnd() != null - ? parseConfiguredTemporalBound(extent.getEnd(), true) - : OffsetDateTime.parse("9999-12-31T23:59:59Z"); - return Optional.of(Interval.of(start.toInstant(), end.toInstant())); - }); - } - - private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { - if (value.contains("T")) { - return OffsetDateTime.parse(value); - } - - return endOfDay - ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) - : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); - } - // TODO: cache deser does not work, because the the feature schemas have no name // (and there are no map keys to get them from) // so to implement this we need the split between cfg and internal schemas diff --git a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java index 076431621..4835716b9 100644 --- a/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java +++ b/xtraplatform-features/src/main/java/de/ii/xtraplatform/features/domain/AbstractFeatureProvider.java @@ -16,7 +16,9 @@ import de.ii.xtraplatform.base.domain.resiliency.Volatile2; import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry; import de.ii.xtraplatform.codelists.domain.Codelist; +import de.ii.xtraplatform.crs.domain.BoundingBox; import de.ii.xtraplatform.crs.domain.CrsInfo; +import de.ii.xtraplatform.crs.domain.CrsTransformationException; import de.ii.xtraplatform.crs.domain.CrsTransformerFactory; import de.ii.xtraplatform.crs.domain.EpsgCrs; import de.ii.xtraplatform.crs.domain.OgcCrs; @@ -39,6 +41,9 @@ import de.ii.xtraplatform.streams.domain.Reactive.Stream; import de.ii.xtraplatform.values.domain.Values; import java.io.IOException; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -56,6 +61,7 @@ import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.threeten.extra.Interval; public abstract class AbstractFeatureProvider< T, U, V extends FeatureProviderConnector.QueryOptions, W extends SchemaBase> @@ -543,6 +549,72 @@ protected Query preprocessQuery(Query query) { return query; } + protected Optional getConfiguredSpatialExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getSpatial); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getSpatial); + + return fromType + .or(() -> fromProvider) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) + .flatMap(extent -> extent.toBoundingBox(getData().getNativeCrs().orElse(OgcCrs.CRS84))); + } + + protected Optional getConfiguredTemporalExtent(String typeName) { + Optional fromType = + Optional.ofNullable(getData().getTypes().get(typeName)) + .flatMap(FeatureSchema::getExtent) + .flatMap(FeatureTypeExtent::getTemporal); + Optional fromProvider = + getData().getExtent().flatMap(FeatureTypeExtent::getTemporal); + + return fromType + .or(() -> fromProvider) + .filter(extent -> !Boolean.TRUE.equals(extent.getComputed())) + .flatMap( + extent -> { + if (extent.getStart() == null && extent.getEnd() == null) { + return Optional.empty(); + } + OffsetDateTime start = + extent.getStart() != null + ? parseConfiguredTemporalBound(extent.getStart(), false) + : OffsetDateTime.parse("0001-01-01T00:00:00Z"); + OffsetDateTime end = + extent.getEnd() != null + ? parseConfiguredTemporalBound(extent.getEnd(), true) + : OffsetDateTime.parse("9999-12-31T23:59:59Z"); + return Optional.of(Interval.of(start.toInstant(), end.toInstant())); + }); + } + + protected Optional transformSpatialExtent( + BoundingBox boundingBox, EpsgCrs targetCrs) { + return crsTransformerFactory + .getTransformer(getData().getNativeCrs().orElse(OgcCrs.CRS84), targetCrs, false) + .flatMap( + crsTransformer -> { + try { + return Optional.of(crsTransformer.transformBoundingBox(boundingBox)); + } catch (CrsTransformationException e) { + return Optional.empty(); + } + }); + } + + private static OffsetDateTime parseConfiguredTemporalBound(String value, boolean endOfDay) { + if (value.contains("T")) { + return OffsetDateTime.parse(value); + } + + return endOfDay + ? LocalDate.parse(value).atTime(23, 59, 59).atOffset(ZoneOffset.UTC) + : LocalDate.parse(value).atStartOfDay().atOffset(ZoneOffset.UTC); + } + @Override public FeatureChanges changes() { return changeHandler;