Skip to content

Positions in reference systems beside the native CRS#577

Merged
azahnen merged 8 commits into
masterfrom
tmp2
Jul 21, 2026
Merged

Positions in reference systems beside the native CRS#577
azahnen merged 8 commits into
masterfrom
tmp2

Conversation

@cportele

Copy link
Copy Markdown
Contributor

Part of addressing ldproxy/ldproxy#1692

Adds support for feature types that store geometries in one of several CRSs.

  • FeatureSchema: a variants declaration on the primary geometry property groups the sibling properties that store a position as recorded; new roles ORIGINAL_GEOMETRY, ORIGINAL_HEIGHT and ORIGINAL_CRS_IDENTIFIER identify the group members and make them internal (readable, but not part of any public schema or regular encoding). Variant properties declare the identifiers of their reference systems (originalCrsIdentifiers), the CRS they are stored in (nativeCrs), the CRS of the recorded positions (originalCrs) and, where needed, a falseEastingDifference.
  • GML decoder: positions are routed by their verbatim srsName to the matching variant property (1D positions as scalars, the identifier stored verbatim); the read pipeline restores originalCrs so encodings receive the recorded positions.
  • Write path: geometries are transformed to the per-property storage CRS.
  • EpsgCrs: new auxiliary alternativeUri for community URIs of a CRS.
  • Collection extents are computed from the primary geometry only.

Also extends the GML decoder for ISO 19139 metadata structures on the wire (value-wrapper chains with attributes and empty elements, xsi:type on wrapper elements), so documents shaped like the encoder's output can be ingested losslessly.

cportele added 4 commits July 14, 2026 17:42
The wire form of an ISO 19139 quantitative result types the anyType
value element via xsi:type (<gco:Record xsi:type="gml:doubleList">) and
carries an empty valueUnit sibling. The decoder rejected any xsi:type;
inside a value-wrap chain the attribute types the content of a value
element rather than substituting a schema type, so it is now tolerated
and dropped there. xsi:type on schema-mapped elements (feature, object
and property elements) remains rejected.

- FeatureTokenDecoderGml: exempt value-wrap chain elements from
  rejectXsiType via the new isValueWrapChainElement, which also
  replaces the equivalent inline condition for pushing VALUE_WRAPPER
  frames
- FeatureTokenDecoderGmlSpec: cover the full genauigkeitswert
  structure including xsi:type and the empty valueUnit element
Some application schemas (e.g. AX_PunktortAU in the German
AFIS-ALKIS-ATKIS NAS schema) carry exactly one position per feature,
but in one of several CRSs - including realizations that map to the
same EPSG code, 1D vertical reference systems, and coordinate forms
whose false easting differs from the CRS definition (Gauss-Krueger
without the zone prefix). Each variant is stored in its own geometry
property with its own storage CRS; the verbatim srsName is stored
alongside so encoders can reproduce it.

- SchemaBase/FeatureSchema: new `crs` option - the storage CRS of a
  geometry property, overriding the provider's nativeCrs
- features-sql: the storage CRS travels as the WKT/WKB operation
  parameters (code, axis-order force); geometry writes route by
  property path, transform from the geometry's CRS to the column's
  storage CRS and emit the column's SRID (inserts and native updates)
- FeatureTokenTransformerCoordinates: geometries with their own
  storage CRS pass through the request-CRS transformation untouched
- geometries: EastingShift coordinates transformation - adds a
  constant to the easting, rounded to micrometres; converts between
  coordinate forms that differ only in the false easting
- gml decoder: GmlGeometryVariants routing on the input profile - a
  position is routed by its verbatim srsName to a variant property
  (with a configured false-easting difference applied) or, for 1D
  vertical reference systems, captured as a scalar value at a FLOAT
  property; the verbatim srsName is emitted at a companion property;
  GeometryDecoderGml exposes the verbatim srsName and consumes 1D
  positions without building a Geometry
The mutation stats collector fed every geometry token into the spatial
extent of the change events, interpreting position variants stored in
a different CRS as native coordinates. Only the primary geometry now
contributes, mirroring the read side, which computes the extent over
the filter/primary geometry column. Also accumulate min/max across
geometries instead of keeping only the last one.
- FeatureSchema: a 'variants' declaration on the primary geometry
  property groups the sibling properties that store the position as
  recorded (crsProperty, verticalProperty, geometryProperties); the new
  roles ORIGINAL_GEOMETRY, ORIGINAL_HEIGHT and ORIGINAL_CRS_IDENTIFIER
  identify the group members (implied when not declared) and make them
  internal: read from the data source, but excluded from queryables,
  sortables, public schemas and regular property encoding
- variant properties declare the identifiers of their reference systems
  (originalCrsIdentifiers), the CRS they are stored in (nativeCrs), the
  CRS of the recorded positions (originalCrs, e.g. the authority axis
  order of a geographic CRS whose stored coordinates follow the GIS
  axis order) and a falseEastingDifference for identifiers whose
  coordinates use a different false easting than the CRS
- VariantsResolver validates the declarations and sets the implied
  roles at provider start
- GML decoder: a position is routed by its verbatim srsName to the
  variant property that lists the identifier, interpreted in
  originalCrs and shifted to conform to nativeCrs; a 1D vertical
  position is captured as a scalar at the vertical property; the
  srsName is stored verbatim at the crsProperty; the routing metadata
  moved from the decoder input profile into the schema
- the read pipeline restores originalCrs so downstream encodings
  receive the positions as recorded
- EpsgCrs: new auxiliary attribute alternativeUri declares an
  identifier under which a CRS is known in a community, used when
  rendering and decoding CRS identifiers on the wire
- new WithoutInternal visitor for deriving public schemas
cportele and others added 3 commits July 16, 2026 15:38
…othing

Predicates on properties that require a join are encoded as INNER-join
semi-joins (A.id IN (SELECT ...)). Negations derived from them were
unsatisfiable or wrong:

- not(<predicate>) was pushed into the subquery by string surgery,
  negating the inner predicate ("some related row does not match")
  instead of the whole predicate ("no related row matches"); features
  without any related row were dropped by the INNER join. The whole
  semi-join is now negated (NOT (...)), which is exact since the outer
  operand is never null. This changes the result of filters like
  NOT role = 'x' on multi-valued properties from "has some other value"
  to "has no value 'x'", the consistent reading of logical negation.
- isNull(<property>) applied IS NULL to the joined column inside the
  subquery, where it can never match. "No value" is now encoded as
  NOT (A.id IN (SELECT ... WHERE <column> IS NOT NULL)).

Also return a 400 instead of a 500 for filter coordinates that cannot be
transformed: a failed transform yields a CoordinateTuple with
isNull()=true, not a null reference; CqlCoordinateChecker now checks it
instead of failing with a NullPointerException.
…the native CRS

With skipUnusedPipelineSteps enabled, the COORDINATES pipeline step was
skipped whenever the requested CRS equals the provider's native CRS. A
geometry property that is stored in its own CRS and declares a differing
originalCrs relies on that step to restore the recorded axis order of its
positions, so such positions were emitted in the stored axis order while
keeping the original srsName. The step is now kept for properties that
declare a nativeCrs with a differing originalCrs, regardless of the
requested CRS.

@azahnen azahnen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cportele
In general I would prefer to put this into an extension, but our current design doesn't really allow that. A more modular approach to the ever-growing FeatureSchema is something I will look into before v5. Two points regarding the PR:

  1. Since variants are a concept specific to geometries, I think the naming of getVariants, SchemaVariants and VariantResolver should reflect that.

  2. getNativeCrs(): Only relevant for properties with type: GEOMETRY in SQL feature providers, when a feature type stores positions in more than one CRS in separate geometry columns.

    Couldn't this also be used for simpler cases with a single geometry column per type but mixed storage CRSs?

@cportele

cportele commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@azahnen

Agreed on a modularization of the feature schema (extensions).

  1. I will update the naming.
  2. That is currently not supported by the specs, which expect that responses have the primary geometry in a single, homogeneous CRS (as an override the PRs define a new profile crs-original). So, also for the "simpler case" we need a primary geometry column with the geometry in the provider-wide native CRS (derived from the geometry in the type-specific native CRS).

The variants concept is specific to geometry properties that store the
same logical position in several CRSs; the generic names did not
reflect that.

- configuration key on geometry properties: variants -> crsVariants
- SchemaVariants -> CrsVariants
- VariantsResolver -> CrsVariantsResolver
- FeatureSchema.getVariants() -> getCrsVariants()
- validation messages and documentation updated accordingly
@cportele

Copy link
Copy Markdown
Contributor Author

@azahnen - the renaming is done (in ldproxy, too).

@azahnen azahnen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cportele Thanks!

Regarding 2, but the spec is not the concern of the provider. If I sent a simple query to the provider requesting raw tokens (to circumvent potential encoder limitations), would it work for a type with a deviating nativeCrs?

@azahnen
azahnen merged commit 7039fd8 into master Jul 21, 2026
3 checks passed
@azahnen
azahnen deleted the tmp2 branch July 21, 2026 09:17
@cportele

Copy link
Copy Markdown
Contributor Author

@azahnen - Regarding your question:

Partially. The token stream itself would work: a geometry property with its own nativeCrs is deliberately excluded from target-CRS reprojection and passed through verbatim, and the write path already transforms to the column CRS per property. But the rest of the provider assumes the provider-wide nativeCrs for the primary geometry: spatial filter literals are encoded in the provider CRS (mixed-SRID against such a column), extents/stats treat primary-geometry coordinates as provider-native, and a crs in the query would silently not apply. So the "simpler case" would need making those parts per-property aware. That's why the docs currently scope it to the CRS variants use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants