Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -311,10 +310,15 @@ public FeatureSchema getSortablesSchema(

@Override
public Optional<BoundingBox> getSpatialExtent(String typeName) {
if (getData().getTypes().containsKey(typeName)) {
if (!getData().getTypes().containsKey(typeName)) {
return Optional.empty();
}

Optional<BoundingBox> configured = getConfiguredSpatialExtent(typeName);
if (configured.isPresent()) {
return configured;
}

try {
Stream<Optional<BoundingBox>> extentGraph =
aggregateStatsReader.getSpatialExtent(
Expand All @@ -337,23 +341,16 @@ public Optional<BoundingBox> getSpatialExtent(String typeName) {
@Override
public Optional<BoundingBox> 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
public Optional<Interval> getTemporalExtent(String typeName) {
return Optional.empty();
if (!getData().getTypes().containsKey(typeName)) {
return Optional.empty();
}

return getConfiguredTemporalExtent(typeName);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -307,7 +306,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;
}

Expand Down Expand Up @@ -353,10 +352,15 @@ public FeatureSchema getSortablesSchema(
@Override
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public Optional<BoundingBox> getSpatialExtent(String typeName) {
if (getData().getTypes().containsKey(typeName)) {
if (!getData().getTypes().containsKey(typeName)) {
return Optional.empty();
}

Optional<BoundingBox> configured = getConfiguredSpatialExtent(typeName);
if (configured.isPresent()) {
return configured;
}

try {
Stream<Optional<BoundingBox>> extentGraph =
aggregateStatsReader.getSpatialExtent(
Expand All @@ -378,23 +382,15 @@ public Optional<BoundingBox> getSpatialExtent(String typeName) {
@Override
public Optional<BoundingBox> 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
public Optional<Interval> getTemporalExtent(String typeName) {
return Optional.empty();
if (!getData().getTypes().containsKey(typeName)) {
return Optional.empty();
}
return getConfiguredTemporalExtent(typeName);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,11 @@ public Optional<BoundingBox> getSpatialExtent(String typeName) {
return Optional.empty();
}

Optional<BoundingBox> configured = getConfiguredSpatialExtent(typeName);
if (configured.isPresent()) {
return configured;
}

String[] cacheKey = {typeName, "stats", "spatial"};
String cacheValidator = getData().getStableHash();

Expand Down Expand Up @@ -1081,18 +1086,7 @@ public Optional<BoundingBox> getSpatialExtent(String typeName) {
@Override
public Optional<BoundingBox> 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
Expand All @@ -1101,6 +1095,11 @@ public Optional<Interval> getTemporalExtent(String typeName) {
return Optional.empty();
}

Optional<Interval> configured = getConfiguredTemporalExtent(typeName);
if (configured.isPresent()) {
return configured;
}

String[] cacheKey = {typeName, "stats", "temporal"};
String cacheValidator = getData().getStableHash();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<W>>
Expand Down Expand Up @@ -543,6 +549,72 @@ protected Query preprocessQuery(Query query) {
return query;
}

protected Optional<BoundingBox> getConfiguredSpatialExtent(String typeName) {
Optional<SpatialExtent> fromType =
Optional.ofNullable(getData().getTypes().get(typeName))
.flatMap(FeatureSchema::getExtent)
.flatMap(FeatureTypeExtent::getSpatial);
Optional<SpatialExtent> 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<Interval> getConfiguredTemporalExtent(String typeName) {
Optional<TemporalExtent> fromType =
Optional.ofNullable(getData().getTypes().get(typeName))
.flatMap(FeatureSchema::getExtent)
.flatMap(FeatureTypeExtent::getTemporal);
Optional<TemporalExtent> 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<BoundingBox> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ default List<CustomFunction> getCql2Functions() {
@Override
Optional<Boolean> 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<FeatureTypeExtent> getExtent();

// custom builder to automatically use keys of types as name of FeatureTypeV2
abstract class Builder<T extends Builder<T>> implements EntityDataBuilder<FeatureProviderDataV2> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ default Type getType() {
*/
Optional<String> getDescription();

/**
* @langEn Optional spatial and temporal extent for this feature type.
* @langDe Optionaler räumlicher und zeitlicher Extent für diesen Feature-Type.
*/
Optional<FeatureTypeExtent> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ public interface FeatureTypeConfiguration {
* @default ""
*/
Optional<String> 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<FeatureTypeExtent> getExtent();
}
Original file line number Diff line number Diff line change
@@ -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 com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.util.Optional;
import org.immutables.value.Value;

/** Extent object for spatial and temporal extents. */
@Value.Immutable
@JsonDeserialize(builder = ImmutableFeatureTypeExtent.Builder.class)
public interface FeatureTypeExtent {
Optional<SpatialExtent> getSpatial();

Optional<TemporalExtent> getTemporal();
}
Loading
Loading