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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"role",
"valueType",
"geometryType",
"geometryTypes",
"objectType",
"label",
"alias",
Expand Down Expand Up @@ -198,6 +199,19 @@ default Type getType() {
@Override
Optional<GeometryType> getGeometryType();

/**
* @langEn Multiple admissible geometry types for properties with `type: GEOMETRY`. Use this
* instead of `geometryType` when more than one geometry type is allowed (e.g. `[POINT,
* MULTI_POINT]`). Values are the same as for `geometryType`.
* @langDe Mehrere zulässige Geometrietypen für Eigenschaften mit `type: GEOMETRY`. Wird anstelle
* von `geometryType` verwendet, wenn mehr als ein Geometrietyp erlaubt ist (z.B. `[POINT,
* MULTI_POINT]`). Werte siehe `geometryType`.
* @default []
* @since v4.8
*/
@Override
List<GeometryType> getGeometryTypes();

/**
* @langEn Optional name for an object type, used for example in JSON Schema. For properties that
* should be mapped as links according to *RFC 8288*, use `Link`.
Expand Down Expand Up @@ -805,6 +819,21 @@ default void concatConstraints() {
}
}

@Value.Check
default void warnOnConflictingGeometryTypes() {
if (getGeometryType().isPresent() && !getGeometryTypes().isEmpty()) {
List<GeometryType> types = getGeometryTypes();
boolean consistent = types.size() == 1 && types.get(0) == getGeometryType().get();
if (!consistent) {
LOGGER.warn(
"Both 'geometryType' ({}) and 'geometryTypes' ({}) are set on property '{}'; 'geometryTypes' takes precedence.",
getGeometryType().get(),
types,
getFullPathAsString());
}
}
}

@Value.Check
default void disallowFlattening() {
Preconditions.checkState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
*/
package de.ii.xtraplatform.features.domain;

import static de.ii.xtraplatform.geometries.domain.GeometryType.ANY;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
Expand Down Expand Up @@ -149,6 +147,8 @@ public static List<Scope> allBut(Scope... scopes) {

Optional<GeometryType> getGeometryType();

List<GeometryType> getGeometryTypes();

Optional<String> getFormat();

Optional<String> getRefType();
Expand Down Expand Up @@ -373,27 +373,36 @@ default List<T> getPrimaryGeometries() {
@JsonIgnore
@Value.Derived
@Value.Auxiliary
default List<GeometryType> getGeometryTypes() {
default List<GeometryType> collectEffectiveGeometryTypes() {
return getPrimaryGeometries().stream()
.map(SchemaBase::getGeometryType)
.filter(Optional::isPresent)
.map(Optional::get)
.map(SchemaBase::getEffectiveGeometryTypes)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
}

@JsonIgnore
@Value.Derived
@Value.Auxiliary
default List<GeometryType> getEffectiveGeometryTypes() {
return getGeometryTypes().isEmpty()
? List.of(getGeometryType().orElse(GeometryType.ANY))
: getGeometryTypes();
}

@JsonIgnore
@Value.Derived
@Value.Auxiliary
default GeometryType getEffectiveGeometryType() {
return getGeometryTypes().stream().reduce((a, b) -> ANY).orElse(ANY);
return GeometryType.effectiveType(
isSpatial() ? getEffectiveGeometryTypes() : collectEffectiveGeometryTypes());
}

@JsonIgnore
@Value.Derived
@Value.Auxiliary
default Optional<Integer> getEffectiveGeometryDimension() {
return getGeometryTypes().stream()
return collectEffectiveGeometryTypes().stream()
.map(GeometryType::getGeometryDimension)
.reduce(
(a, b) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,7 @@ protected T deriveValueSchema(FeatureSchema schema) {
break;
case GEOMETRY:
valueSchema =
getSchemaForGeometry(
schema.getGeometryType().orElse(GeometryType.ANY), label, description, role);
getSchemaForGeometry(schema.getEffectiveGeometryTypes(), label, description, role);
break;
case OBJECT:
case OBJECT_ARRAY:
Expand Down Expand Up @@ -438,7 +437,7 @@ protected abstract T getSchemaForLiteralType(
Optional<String> codelistId);

protected abstract T getSchemaForGeometry(
GeometryType geometryType,
List<GeometryType> geometryTypes,
Optional<String> title,
Optional<String> description,
Optional<String> role);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* 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.geometries.domain.GeometryType
import spock.lang.Specification

class FeatureSchemaGeometryTypesSpec extends Specification {

static FeatureSchema geom(GeometryType single, List<GeometryType> multi) {
def b = new ImmutableFeatureSchema.Builder()
.name("g")
.type(SchemaBase.Type.GEOMETRY)
.sourcePath("g")
if (single != null) {
b.geometryType(single)
}
if (multi != null) {
b.geometryTypes(multi)
}
return b.build()
}

def "property with no geometry types: effective is ANY, list contains ANY"() {
when:
def schema = geom(null, null)

then:
schema.effectiveGeometryType == GeometryType.ANY
schema.effectiveGeometryTypes == [GeometryType.ANY]
}

def "property with only geometryType: effective is that type"() {
when:
def schema = geom(GeometryType.POINT, null)

then:
schema.effectiveGeometryType == GeometryType.POINT
schema.effectiveGeometryTypes == [GeometryType.POINT]
}

def "property with single entry in geometryTypes: effective is that entry"() {
when:
def schema = geom(null, [GeometryType.MULTI_POLYGON])

then:
schema.effectiveGeometryType == GeometryType.MULTI_POLYGON
schema.effectiveGeometryTypes == [GeometryType.MULTI_POLYGON]
}

def "property with two simple-feature entries: effective is ANY"() {
when:
def schema = geom(null, [GeometryType.POINT, GeometryType.MULTI_POINT])

then:
schema.effectiveGeometryType == GeometryType.ANY
schema.effectiveGeometryTypes.toSet() ==
[GeometryType.POINT, GeometryType.MULTI_POINT].toSet()
}

def "property with non-simple-feature entry: effective is ANY_EXTENDED"() {
when:
def schema = geom(null, [GeometryType.LINE_STRING, GeometryType.CIRCULAR_STRING])

then:
schema.effectiveGeometryType == GeometryType.ANY_EXTENDED
}

def "property with three curve entries: effective is ANY_EXTENDED"() {
when:
def schema = geom(null, [
GeometryType.LINE_STRING,
GeometryType.CIRCULAR_STRING,
GeometryType.COMPOUND_CURVE
])

then:
schema.effectiveGeometryType == GeometryType.ANY_EXTENDED
}

def "property with both fields set consistently: result is that single type"() {
when:
def schema = geom(GeometryType.POINT, [GeometryType.POINT])

then:
schema.effectiveGeometryType == GeometryType.POINT
}

def "property with both fields set differently: geometryTypes wins"() {
when:
def schema = geom(GeometryType.POINT, [GeometryType.POINT, GeometryType.MULTI_POINT])

then:
schema.effectiveGeometryType == GeometryType.ANY
}

/** Builds a concat'd feature whose branches each carry one PRIMARY_GEOMETRY of the given type. */
static FeatureSchema concatFeature(GeometryType... branchTypes) {
def branches = branchTypes.toList().withIndex().collect { GeometryType t, int i ->
new ImmutableFeatureSchema.Builder()
.name("branch${i}" as String)
.sourcePath("/branch${i}" as String)
.putProperties2("id", new ImmutableFeatureSchema.Builder()
.sourcePath("id")
.type(SchemaBase.Type.INTEGER)
.role(SchemaBase.Role.ID))
.putProperties2("geometry", new ImmutableFeatureSchema.Builder()
.sourcePath("geom")
.type(SchemaBase.Type.GEOMETRY)
.geometryType(t)
.role(SchemaBase.Role.PRIMARY_GEOMETRY))
.build()
}
return new ImmutableFeatureSchema.Builder()
.name("test")
.type(SchemaBase.Type.OBJECT_ARRAY)
.concat(branches)
.build()
}

def "non-concat feature: getPrimaryGeometries() has 0 or 1 entry"() {
given:
def withGeom = new ImmutableFeatureSchema.Builder()
.name("test").type(SchemaBase.Type.OBJECT).sourcePath("/t")
.putProperties2("id", new ImmutableFeatureSchema.Builder()
.sourcePath("id").type(SchemaBase.Type.INTEGER).role(SchemaBase.Role.ID))
.putProperties2("geometry", new ImmutableFeatureSchema.Builder()
.sourcePath("geom").type(SchemaBase.Type.GEOMETRY)
.geometryType(GeometryType.POINT).role(SchemaBase.Role.PRIMARY_GEOMETRY))
.build()
def withoutGeom = new ImmutableFeatureSchema.Builder()
.name("test").type(SchemaBase.Type.OBJECT).sourcePath("/t")
.putProperties2("id", new ImmutableFeatureSchema.Builder()
.sourcePath("id").type(SchemaBase.Type.INTEGER).role(SchemaBase.Role.ID))
.build()

expect:
withGeom.primaryGeometries.size() == 1
withGeom.collectEffectiveGeometryTypes() == [GeometryType.POINT]
withGeom.effectiveGeometryType == GeometryType.POINT
withoutGeom.primaryGeometries.isEmpty()
withoutGeom.collectEffectiveGeometryTypes().isEmpty()
withoutGeom.effectiveGeometryType == GeometryType.ANY
}

def "concat with two branches, same primary geometry type: list has 2 entries, effective is that type"() {
when:
def schema = concatFeature(GeometryType.MULTI_POLYGON, GeometryType.MULTI_POLYGON)

then:
schema.primaryGeometries.size() == 2
schema.collectEffectiveGeometryTypes() == [GeometryType.MULTI_POLYGON]
schema.effectiveGeometryType == GeometryType.MULTI_POLYGON
}

def "concat with branches POINT + MULTI_POINT: list has 2 entries, effective is ANY"() {
when:
def schema = concatFeature(GeometryType.POINT, GeometryType.MULTI_POINT)

then:
schema.primaryGeometries.size() == 2
schema.collectEffectiveGeometryTypes().toSet() ==
[GeometryType.POINT, GeometryType.MULTI_POINT].toSet()
schema.effectiveGeometryType == GeometryType.ANY
}

def "concat with three differing simple-feature branches: effective is ANY"() {
when:
def schema = concatFeature(
GeometryType.POINT, GeometryType.LINE_STRING, GeometryType.POLYGON)

then:
schema.primaryGeometries.size() == 3
schema.collectEffectiveGeometryTypes().toSet() ==
[GeometryType.POINT, GeometryType.LINE_STRING, GeometryType.POLYGON].toSet()
schema.effectiveGeometryType == GeometryType.ANY
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package de.ii.xtraplatform.geometries.domain;

import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -61,6 +62,21 @@ public static boolean onlySimpleFeatureGeometries(Set<GeometryType> geometryType
return geometryTypes.stream().allMatch(GeometryType::isSimpleFeature);
}

/**
* Collapses a list of admissible geometry types to a single effective type: empty -> {@code ANY};
* one entry -> that entry; more than one -> {@code ANY} when all entries are simple-feature
* types, otherwise {@code ANY_EXTENDED}.
*/
public static GeometryType effectiveType(Collection<GeometryType> geometryTypes) {
if (geometryTypes.isEmpty()) {
return ANY;
}
if (geometryTypes.size() == 1) {
return geometryTypes.iterator().next();
}
return onlySimpleFeatureGeometries(Set.copyOf(geometryTypes)) ? ANY : ANY_EXTENDED;
}

public Optional<Integer> getGeometryDimension() {
return Optional.ofNullable(geometryDimension);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public interface JsonSchemaBuildingBlocks {
new ImmutableJsonSchemaGeometry.Builder().format("geometry-circularstring").build();
JsonSchemaGeometry COMPOUND_CURVE =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-compoundcurve").build();
JsonSchemaGeometry CURVE =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-curve").build();
JsonSchemaGeometry MULTI_LINE_STRING =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-multilinestring").build();
JsonSchemaGeometry LINE_STRING_OR_MULTI_LINE_STRING =
Expand All @@ -40,8 +38,6 @@ public interface JsonSchemaBuildingBlocks {
new ImmutableJsonSchemaGeometry.Builder().format("geometry-polygon").build();
JsonSchemaGeometry CURVE_POLYGON =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-curvepolygon").build();
JsonSchemaGeometry SURFACE =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-surface").build();
JsonSchemaGeometry POLYHEDRAL_SURFACE =
new ImmutableJsonSchemaGeometry.Builder().format("geometry-polyhedralsurface").build();
JsonSchemaGeometry MULTI_POLYGON =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
*/
package de.ii.xtraplatform.jsonschema.domain;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.hash.Funnel;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import org.immutables.value.Value;

@Value.Immutable
Expand All @@ -20,9 +23,14 @@ public abstract class JsonSchemaGeometry extends JsonSchema {
public static final Funnel<JsonSchemaGeometry> FUNNEL =
(from, into) -> {
into.putString(from.getFormat(), StandardCharsets.UTF_8);
from.getGeometryTypes()
.ifPresent(types -> types.forEach(t -> into.putString(t, StandardCharsets.UTF_8)));
};

public abstract String getFormat();

@JsonProperty("x-ldproxy-geometryTypes")
public abstract Optional<List<String>> getGeometryTypes();

public abstract static class Builder extends JsonSchema.Builder {}
}
Loading