navibyte / geospatial

Geospatial data structures, tools and utilities for Dart and Flutter.
Other
51 stars 5 forks source link

Handling CRS ids from different sources when encoding/decoding external data and managing in-memory objets #226

Open navispatial opened 2 months ago

navispatial commented 2 months ago

This is not a trivial task to implement as a common solution.

Geospatial data formats

GeoJSON

GeoJSON (RFC 7946)

JSON-FG

See #53

A reference system can be specified in a JSON-FG document using a "coordRefSys" member...:

WKT

Well-known text representation of geometry (WKT)

WKB

Well-known binary representation of geometry (WKB)

EWKT

CRS identifier as an integer (assuming an EPSG code here) just like wikipedia page describes:

SRID=4326;POINT(-44.3 60.1)

See PostGIS: ST_GeomFromEWKT for more examples.

EWKB

See

SRID integer can be encoded after wkbType in binary encoding.

Geobase 1.0.x implementation

CoordRefSys class

CoordRefSys

Related class to provide logic, can be customized by extending and registering: CoordRefSysResolver.

Feature and Geometry classes

Feature and geometry objects are specified by the vector_data sub package:

These classes do not have members for CRS information. It's assumed that a CRS applied is known by an application context, or stored outside feature and geometry objects. This should work as long as all feature and geometry objects share a same CRS.

Some encoding / decoding methods has a parameter allowing passing such CRS information to a encoder / decoder.

For example Point:

/// Parses a point geometry from text conforming to format.
Point.parse(String text, {TextReaderFormat<SimpleGeometryContent> format = GeoJSON.geometry, CoordRefSys? crs, Map<String, dynamic>? options});

/// The string representation of this geometry object, with format applied.
String toText({TextWriterFormat<SimpleGeometryContent> format = GeoJSON.geometry, int? decimals, CoordRefSys? crs, Map<String, dynamic>? options});

Text format

Such a crs parameter is passed to a text format object that has following definition:

TextWriterFormat

/// Returns a text format decoder that decodes text as Content to builder.
ContentDecoder decoder(Content builder, {CoordRefSys? crs, Map<String, dynamic>? options});

TextWriterFormat

/// Returns a text format encoder for Content.
ContentEncoder<Content> encoder({StringSink? buffer, int? decimals, CoordRefSys? crs, Map<String, dynamic>? options}); 

An optional CoordRefSys? crs is then passed to actual decoder or encoder implementation, that can use crs information for example to handle axis order logic for all geospatial features and geometries applied.

Content interfaces

Decoders, encoders and object builder also use "content interfaces" to send decoded or encoded elements, for example SimpleGeometryContent:

/// Writes a point geometry with position.
void point(Position position, {String? name});

/// Writes a line string geometry with a chain of positions.
void lineString(PositionSeries chain, {String? name, Box? bounds});

/// ....

Currently methods on these content interfaces do not have a parameter for a geometry or feature instance specific CRS information (only decoder / encoder specific common value is available for some formats).

Issues with current implementation

No issues when decoding/encoding GeoJSON, WKT or WKB data.

However supporting EWKT, EWKB and JSON-FG properly might require a way to pass geometry or feature object specific CRS identifier between decoders, encoders and object builders.

And supporting instantiating an object tree parsed from a JSON-FG document might require an optional CRS member for each feature and geometry objects. So that when null either a default CRS (like WGS 84) or a CRS from a parent object would be assumed.

Geobase 1.1.x proposal

See #165 both decoding and encoding of EWKB can be implemented without major changes.

Geobase 2.0.x proposal

CoordRefSys class

Should be ensured that integer ids (from EWKB or EWKT) and different type of URI, CURIE, lists of URIs / CURIEs, URIs or CURIEs with epoch data (that are going to be used by JSON-FG) etc. can be parsed and used.

Add some basic logic to CoordRefSysResolver to cover most common reference systems.

Feature and Geometry classes

Add the CoordRefSys? crs member to feature and geometry objects with backward compatibility in mind. Such information would be totally optional, so by default null value would be stored.

New constructors / static factories might be needed that allow storing an instance of CoordRefSys to crs member.

Content interfaces

Changes to signatures of all methods building a geometry or a feature would be needed. For example:

/// Writes a point geometry with position.
void point(Position position, {String? name, CoordRefSys? crs});

/// Writes a line string geometry with a chain of positions.
void lineString(PositionSeries chain, {String? name, Box? bounds, CoordRefSys? crs});

/// ....

Decoding

Decoders / parsers could use changed methods to pass a geometry or feature specific CRS information to consumers.

As a consumer for example object builders (like GeometryBuilder and FeatureBuilder) would then use any non-null crs data when instantiating geometries or features.

Encoding

Similarly when encoding / writing a geometry or feature object to external data format.

Currently Point has methods to write as text format:

  @override
  void writeTo(SimpleGeometryContent writer, {String? name}) =>
      isEmptyByGeometry
          ? writer.emptyGeometry(Geom.point, name: name)
          : writer.point(position, name: name);

  @override
  String toText({
    TextWriterFormat<SimpleGeometryContent> format = GeoJSON.geometry,
    int? decimals,
    CoordRefSys? crs,
    Map<String, dynamic>? options,
  }) {
    final encoder =
        format.encoder(decimals: decimals, crs: crs, options: options);
    writeTo(encoder.writer);
    return encoder.toText();
  }

This should be changed to something like this:

  @override
  void writeTo(SimpleGeometryContent writer, {String? name}) =>
      isEmptyByGeometry
          ? writer.emptyGeometry(Geom.point, name: name)
          : writer.point(position, name: name, crs: this.crs);

  @override
  String toText({
    TextWriterFormat<SimpleGeometryContent> format = GeoJSON.geometry,
    int? decimals,
    CoordRefSys? crs,
    Map<String, dynamic>? options,
  }) {
    final encoder =
        format.encoder(decimals: decimals, crs: this.crs ?? crs, options: options);
    writeTo(encoder.writer);
    return encoder.toText();
  }

Target version

Changes described above can be implemented with backward compatibility - at least for package user, however if someone has extended classes mentioned above, then there might be some risks.

As package API changes are needed to multiple sub packages it would be most safe to target geobase 2.0.0 version with changes described.

Also #29, #53, #165 should be associated to these changes too.