Skip to content

Vector formats

The geobase package supports common geospatial data formats:

  • GeoJSON text representation for feature and geometry objects
  • WKT text representation for geometry objects
  • WKB binary representation for geometry objects

The latest version 1.1.0 (published on April 2024) provides also following extensions or variants:

  • Newline-delimited GeoJSON providing an optimized format for large datasets with large number of features
  • Extended WKT (EWKT) supporting geometries with a coordinate reference system id (SRID)
  • Extended WKB (EWKB) supporting geometries with a coordinate reference system id (SRID)

Format classes

Some of the formats support encodings both for simple geometries and geospatial features, and some only for one of these object types.

The table below shows support in each data format and which static constant instance of TextFormat or BinaryFormat should be used to reference a certain format.

Vector formatSimple geometriesGeospatial features
GeoJSONGeoJSON.geometryGeoJSON.feature
Newline-delimited GeoJSON-GeoJSONL.feature
WKT (and EWKT)WKT.geometry-
WKB (and EWKB)WKB.geometry-

GeoJSON, GeoJSONL and WKT are text formats extending the base class TextFormat.

More specifically for example GeoJSON provides the type TextFormat<GeometryContent> for simple geometries and TextFormat<FeatureContent> for geospatial features.

For simple geometries WKT.geometry is a text format of the type TextFormat<GeometryContent> and WKB.geometry is a binary format of the type BinaryFormat<GeometryContent>.

Thatā€™s about class theory, next letā€™s see how these classes are used in action.

GeoJSON

WGS 84 longitude/latitude

As already described GeoJSON is a format for encoding geometry, feature and feature collection objects. The data structures introduced on previous simple geometries and geospatial features chapters are modelled to support encoding and decoding GeoJSON data.

The format is specified by the RFC 7946 standard. Geometry objects use WGS 84 longitude/latitude geographic coordinates. Also alternative coordinate reference systems can be used when involved parties have a prior arrangement of using other systems.

In this package the default coordinate reference system (WGS 84 with longitude before latitude) can also be referenced by the CoordRefSys.CRS84 constant. Normally when parsing and writing content in this default coordinate system you donā€™t need to specify a crs however.

Parsing a feature collection from a GeoJSON text representation is demonstrated below.

// sample GeoJSON text representation (a feature collection with two features)
const sample = '''
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "ROG",
"geometry": {
"type": "Point",
"coordinates": [-0.0014, 51.4778, 45.0]
},
"properties": {
"title": "Royal Observatory",
"place": "Greenwich"
}
},
{
"type": "Feature",
"id": "TB",
"geometry": {
"type": "Point",
"coordinates": [-0.075406, 51.5055]
},
"properties": {
"title": "Tower Bridge",
"built": 1886
}
}
]
}
''';
// parse a FeatureCollection object using the decoder of the GeoJSON format
final collection = FeatureCollection.parse(sample, format: GeoJSON.feature);
// loop through features and print id, geometry and properties for each
for (final feature in collection.features) {
print('Feature with id: ${feature.id}');
print(' geometry: ${feature.geometry}');
print(' properties:');
for (final key in feature.properties.keys) {
print(' $key: ${feature.properties[key]}');
}
}

You can also parse separate geometry objects without any parent feature objects.

// Read GeoJSON content with coordinate order: longitude, latitude, elevation.
final point = Point.parse(
'{"type": "Point", "coordinates": [-0.0014, 51.4778, 45.0]}',
format: GeoJSON.geometry,
);

Encoding GeoJSON text representation from feature and geometry objects is an easy task to do also. Just call the toText() method on any such object instance.

// Prints the GeoJSON text representation of a geometry object named "point".
print(point.toText(format: GeoJSON.geometry));

Newline-delimited GeoJSON

GeoJSON is a perfect textual data format for sharing geospatial structured data between servers and clients in web or mobile apps. However it may become suboptimal in use cases that use very large number of geospatial feature objects or that stream such objects one by one.

One solution for these scenarios is called Newline-delimited GeoJSON.

Regardless of what itā€™s called or what minor variations there are in specifications mentioned the key idea is simple.

A text file conforming to this format represents one feature collection (however without FeatureCollection element encoded). Such a file may contain any number of features that are separated by the newline character ā€œ\nā€. Each line separately must contain a valid GeoJSON feature object.

A newline-delimited GeoJSON representation of same sample data as used already in samples earlier is presented below.

{"type":"Feature","id":"ROG","geometry":{"type":"Point","coordinates":[-0.0014,51.4778,45]},"properties":{"title":"Royal Observatory","place":"Greenwich"}}
{"type":"Feature","id":"TB","geometry":{"type":"Point","coordinates":[-0.075406,51.5055]},"properties":{"title":"Tower Bridge","built":1886}}

Decoding and encoding newline-delimited GeoJSON is not any harder than using the standard one. Just remember to use a format called ā€œGeoJSONLā€ instead of ā€œGeoJSONā€.

/// a feature collection encoded as GeoJSONL and containing two features that
/// are delimited by the newline character \n
const sample = '''
{"type":"Feature","id":"ROG","geometry":{"type":"Point","coordinates":[-0.0014,51.4778,45]},"properties":{"title":"Royal Observatory","place":"Greenwich"}}
{"type":"Feature","id":"TB","geometry":{"type":"Point","coordinates":[-0.075406,51.5055]},"properties":{"title":"Tower Bridge","built":1886}}
''';
// parse a FeatureCollection object using the decoder for the GeoJSONL format
final collection = FeatureCollection.parse(sample, format: GeoJSONL.feature);
// ... use features read and returned in a feature collection object ...
// encode back to GeoJSONL data
print(collection.toText(format: GeoJSONL.feature, decimals: 5));

Advantages of using GeoJSONL over the standard GeoJSON include efficiency when streaming or storing very large number geospatial features. Itā€™s also much simpler to decode newline-delimited GeoJSON data than hierarchically structured standard GeoJSON data. A client could also skip some features on a stream without parsing all data.

The decoder provided by geobase supports reading features delimited by newline (\n), carriage-return followed by newline (\r\n) and record-separator (RS) characters. By default the encoder delimits features using the \n character.

Alternative coordinate reference systems

When using GeoJSON to represent geospatial data in ā€œalternative coordinate reference systemsā€, such a system must be explicitely defined (and known) before reading in or before writing out GeoJSON content.

As described in the coordinates summary internally all classes in this package handle coordinate axis order so that x (or longitude) is always before y (or latitude). However some coordinate reference systems require other axis order when representing geometries in external data formats.

The CoordRefSys class introduced in the section about coordinate reference systems has the swapXY method that tells how axis order should be handled for a certain coordinate reference system when dealing with external data representations (like the current specification of GeoJSON) that do not specify a generic axis order for alternative coordinate reference systems.

The sample below demonstrates the logic:

// CRS for geographic coordinates with latitude before longitude in GeoJSON.
const epsg4326 = CoordRefSys.EPSG_4326;
// Read GeoJSON content with coordinate order: longitude, latitude, elevation.
final point1 = Point.parse(
'{"type": "Point", "coordinates": [-0.0014, 51.4778, 45.0]}',
// no CRS must be specified for the default coordinate reference system:
// `CoordRefSys.CRS84` or `http://www.opengis.net/def/crs/OGC/1.3/CRS84`
);
final pos1 = Geographic.from(point1.position);
// prints: Point1: lon: 0.0014Ā°W lat: 51.4778Ā°N
print('Point1: lon: ${pos1.lonDms()} lat: ${pos1.latDms()}');
// Read GeoJSON content with coordinate order: latitude, longitude, elevation.
final point2 = Point.parse(
'{"type": "Point", "coordinates": [51.4778, -0.0014, 45.0]}',
crs: epsg4326, // CRS must be explicitely specified
);
final pos2 = Geographic.from(point2.position);
// prints: Point2: lon: 0.0014Ā°W lat: 51.4778Ā°N
print('Point2: lon: ${pos2.lonDms()} lat: ${pos2.latDms()}');
// Both `point1` and `point2` store coordinates internally in this order:
// longitude, latitude, elevation.
// Writing GeoJSON without crs information expects longitude-latitude order.
// Prints: {"type":"Point","coordinates":[-0.0014,51.4778,45]}
print(point2.toText(format: GeoJSON.geometry));
// Writing with crs (EPSG:4326) results in latitude-longitude order.
// Prints: {"type":"Point","coordinates":[51.4778,-0.0014,45]}
print(point2.toText(format: GeoJSON.geometry, crs: epsg4326));

WKT

Well-known text representation of geometry

Well-known text representation of geometry (WKT) is a text markup language for representing vector geometry objects (but not applicable for feature objects). Itā€™s specified by the Simple Feature Access - Part 1: Common Architecture published by OGC.

WKT is used, for example, by PostGIS that provides functions converting geometries to and from a WKT representation. This allows geometry objects to be easily human readable. WKT geometries are used also by many other OGC specifications and supported by different geospatial applications. In web you can find even more guides how to use WKT.

The syntax for WKT is quite simple. A geometry element is identified by a tag (ie. POINT for point geometries, LINESTRING for polylines and POLYGON for polygons) and an optional specifier denoting extra coordinate dimension (Z or M). Coordinate values are included inside parentheses. For 2D coordinates X is always the first and Y the second coordinate value (and when representing geographic coordinates then X = longitude and Y = latitude in this order).

The snippet below shows Dart code parsing 2D, 3D, measured and 4D point geometries represented as WKT text.

// parse a Point geometry with a 2D position (x, y) from WKT text
Point.parse('POINT(-0.0014 51.4778)', format: WKT.geometry);
// parse a Point geometry with a 3D position (x, y, z) from WKT text
Point.parse('POINT Z(-0.0014 51.4778 45.0)', format: WKT.geometry);
// parse a Point geometry with a measured position (x, y, m) from WKT text
Point.parse('POINT M(-0.0014 51.4778 10.0)', format: WKT.geometry);
// parse a Point geometry with a measured position (x, y, z, m) from WKT text
Point.parse('POINT ZM(-0.0014 51.4778 45.0 10.0)', format: WKT.geometry);

If geometry type is not known when parsing text from external datasources, you can use GeometryBuilder to parse geometries of any type:

const geometriesWkt = [
'POINT Z(10.123 20.25 -30.95)',
'LINESTRING(-1.1 -1.1, 2.1 -2.5, 3.5 -3.49)',
];
for(final geomWkt in geometriesWkt) {
// parse geometry (Point and LineString inherits from Geometry)
final Geometry geom = GeometryBuilder.parse(geomWkt, format: WKT.geometry);
if(geom is Point) {
// do something with point geometry
} else if(geom is LineString) {
// do something with line string geometry
}
}

Just like with GeoJSON, also writing an encoded text representation is supported too.

// writes a Point geometry with a 3D position (x, y, z) to WKT text
Point.build([-0.0014, 51.4778, 45.0]).toText(format: WKT.geometry);

Itā€™s possible to encode geometry data as WKT text also without creating geometry objects first. However this requires accessing an encoder instance from the WKT format, and then writing content to that encoder. See sample below:

// geometry text format encoder for WKT
const format = WKT.geometry;
final encoder = format.encoder();
// prints:
// POINT ZM(10.123 20.25 -30.95 -1.999)
encoder.writer.point(
[10.123, 20.25, -30.95, -1.999].xyzm,
);
print(encoder.toText());

Such format encoders (and formatting without geometry objects) are suppported also for GeoJSON. However for both WKT and GeoJSON encoding might be easier using concrete geometry model objects.

Extended WKT (EWKT)

For some use cases the standard WKT is not enough.

Extended WKT (or EWKT) is a PostGIS-specific flavor of this format. It has an alternative syntax for specifying extra coordinates (Z and M), and it allows encoding also a coordinate reference system (SRID).

Let the sample below tell more!

const wktPoints = [
/// A 2D point represented as WKT text.
'POINT(-0.0014 51.4778)',
/// A 3D point represented as WKT text.
'POINT Z(-0.0014 51.4778 45)',
/// A 3D point with SRID represented as EWKT text.
'SRID=4326;POINT(-0.0014 51.4778 45)',
/// A measured point represented as EWKT text.
'POINTM(-0.0014 51.4778 100.0)',
];
// decode SRID, s coordType and a point geometry (with a position) from input
for (final p in wktPoints) {
final srid = WKT.decodeSRID(p);
final coordType = WKT.decodeCoordType(p);
final pos = Point.parse(p, format: WKT.geometry).position;
print('$srid $coordType ${pos.x} ${pos.y} ${pos.optZ} ${pos.optM}');
}
// the previous sample prints:
// null Coords.xy -0.0014 51.4778 null null
// null Coords.xyz -0.0014 51.4778 45.0 null
// 4326 Coords.xyz -0.0014 51.4778 45.0 null
// null Coords.xym -0.0014 51.4778 null 100.0

WKB

Well-known binary

According to the Wikipedia description a binary equivalent for WKT, known as well-known binary (WKB), is used to transfer and store the same information in a more compact form convenient for computer processing but that is not human-readable.

The standard WKB binary representation supports encoding the same geometry and coordinate types as the WKT text representation.

Extended WKB (EWKB)

The WKB class supports also a variant of WKB called Extended WKB (EWKB) that is a PostGIS-specific format. Geometry types for 3D (with z coordinates) and measured (with m) coordinates are encoded differently to the standard WKB. Itā€™s also possible to encode an optional SRID (or coordinate reference system id) to EWKB data thatā€™s not possible with the standard WKB.

More information about EWKB can be read from PostGIS or GEOS software documentation.

Basic sample on WKB

The sample below shows how to deal with data that could contain either WKB or EWKB binary data.

// to get a sample point, first parse a 3D point from WKT encoded string
final p = Point.parse('POINT Z(-0.0014 51.4778 45)', format: WKT.geometry);
// to encode a geometry as WKB/EWKB use toBytes() or toBytesHex() methods
// encode as standard WKB data (format: `WKB.geometry`), prints:
// 01e9030000c7bab88d06f056bfb003e78c28bd49400000000000804640
final wkbHex = p.toBytesHex(format: WKB.geometry);
print(wkbHex);
// encode as Extended WKB data (format: `WKB.geometryExtended`), prints:
// 0101000080c7bab88d06f056bfb003e78c28bd49400000000000804640
final ewkbHex = p.toBytesHex(format: WKB.geometryExtended);
print(ewkbHex);
// otherwise encoded data equals, but bytes for the geometry type varies
// there are some helper methods to analyse WKB/EWKB bytes or hex strings
// (decodeFlavor, decodeEndian, decodeSRID and versions with hex postfix)
// prints: "WkbFlavor.standard - WkbFlavor.extended"
print('${WKB.decodeFlavorHex(wkbHex)} - ${WKB.decodeFlavorHex(ewkbHex)}');
// when decoding WKB or EWKB data, a variant is detected automatically, so
// both `WKB.geometry` and `WKB.geometryExtended` can be used
final pointFromWkb = Point.decodeHex(wkbHex, format: WKB.geometry);
final pointFromEwkb = Point.decodeHex(ewkbHex, format: WKB.geometry);
print(pointFromWkb.equals3D(pointFromEwkb)); // prints "true"
// SRID can be encoded only on EWKB data, this sample prints:
// 01010000a0e6100000c7bab88d06f056bfb003e78c28bd49400000000000804640
final ewkbHexWithSRID =
p.toBytesHex(format: WKB.geometryExtended, crs: CoordRefSys.EPSG_4326);
print(ewkbHexWithSRID);
// if you have WKB or EWKB data, but not sure which, then you can fist check
// a flavor and whether it contains SRID, prints: "SRID from EWKB data: 4326"
if (WKB.decodeFlavorHex(ewkbHexWithSRID) == WkbFlavor.extended) {
final srid = WKB.decodeSRIDHex(ewkbHexWithSRID);
if (srid != null) {
print('SRID from EWKB data: $srid');
// after finding out CRS, an actual point can be decoded
// Point.decodeHex(ewkbHexWithSRID, format: WKB.geometry);
}
}

Advanced sample on WKB

Two different approaches to use WKB encoders and decoders are presented in this section.

First a not-so-simple sample below processes data for demo purposes in following steps:

  1. write geometry content as a source
  2. encode content as WKB bytes
  3. decode those WKB bytes
  4. WKT encoder receives input from WKB decoder, and prints WKT text
// geometry binary format encoder for WKB
const format = WKB.geometry;
final encoder = format.encoder();
// write geometries (here only point) to content writer of the encoder
encoder.writer.point(
[10.123, 20.25, -30.95, -1.999].xyzm,
);
// get encoded bytes (Uint8List) and Base64 encoded text (String)
final wkbBytes = encoder.toBytes();
final wkbBytesAsBase64 = encoder.toText();
// prints (point encoded to WKB binary data, formatted as Base64 text):
// AAAAC7lAJD752yLQ5UA0QAAAAAAAwD7zMzMzMzO///vnbItDlg==
print(wkbBytesAsBase64);
// next decode this WKB binary data and use WKT text format encoder as target
// geometry text format encoder for WKT
final wktEncoder = WKT.geometry.encoder();
// geometry binary format decoder for WKB
// (with content writer of the WKT encoder set as a target for decoding)
final decoder = WKB.geometry.decoder(wktEncoder.writer);
// now decode those WKB bytes (Uint8List) created already at the start
decoder.decodeBytes(wkbBytes);
// finally print WKT text:
// POINT ZM(10.123 20.25 -30.95 -1.999)
print(wktEncoder.toText());

The solution above can be simplified a lot by using geometry model objects:

// create a Point object
final point = Point.build([10.123, 20.25, -30.95, -1.999]);
// get encoded bytes (Uint8List)
final wkbBytes = point.toBytes(format: WKB.geometry);
// at this point our WKB bytes could be sent to another system...
// then create a Point object, but now decoding it from WKB bytes
final pointDecoded = Point.decode(wkbBytes, format: WKB.geometry);
// finally print WKT text:
// POINT ZM(10.123 20.25 -30.95 -1.999)
print(pointDecoded.toText(format: WKT.geometry));

This second solution uses same formats, encoders, decoders and builders as the first one, but the details of using them is hidden under an easier interface.