Decode and encode GeoJSON, WKT and WKB in Dart and Flutter apps

Navibyte
8 min readApr 22, 2024

--

Source: Mwtoews, CC BY-SA 3.0, via Wikimedia Commons

Geospatial tools for Dart has new updates with the latest geobase package version 1.1.0 (April 2024) available on pub.dev!

This package has capabilities for reading and writing simple geometry and feature objects with commonly used GeoJSON, WKT and WKB geospatial data formats.

How to use these data formats in Dart and Flutter apps? Read the article to get a good introduction on the subject!

The latest version 1.1.0 also supports newline-delimited GeoJSON, Extended WKT (EWKT) and Extended WKB (EWKB). All code examples presented here are based on this version.

Let’s start with some basic geospatial concepts adapted to the world of Dart programming...

Simple geometries for points, lines and polygons

The geometry model implemented by the geobase package is based on Simple Feature Access — Part 1: Common Architecture published by the Open Geospatial Consortium (OGC).

The basic primitives are point, line string (or a polyline) and polygon, and their multi-part representations. A geometry collection may contain any type of other geometry objects.

This is best illustrated by an example with code supported by the geobase package.

Source (shapes): Mwtoews, CC BY-SA 3.0, via Wikimedia Commons

Geometry objects above are created from flat double arrays representing sequences of 2D positions (each with x and y coordinates).

There are also other factories available for each geometry type — please check the API reference. Also 3D (with z coordinate) and measured (with m coordinate) positions are supported.

Geospatial features and collections

A geospatial feature is an entity with an identifier, a geometry and properties. Features are used to represent real-world objects like roads, buildings, cities, electric lines, etc.

Below is an illustration of features in a simple vector map. Wells are features with point geometries, rivers with line strings (or polyline) geometries, and finally lakes are features with polygon geometries. Features normally contain also an identifier and other attributes (or properties) along with a geometry.

Source: Mwtoews, CC BY-SA 3.0, via Wikimedia Commons

Sets of features are contained by feature collections.

A sample Dart code below creates a feature collection containing two features.

// A geospatial feature collection (with two features):
FeatureCollection([
Feature(
id: 'ROG',
// a point geometry with a position (lon, lat, elev)
geometry: Point.build([-0.0014, 51.4778, 45.0]),
properties: {
'title': 'Royal Observatory',
'place': 'Greenwich',
'city': 'London',
'isMuseum': true,
'measure': 5.79,
},
),
Feature(
id: 'TB',
// a point geometry with a position (lon, lat)
geometry: Point.build([-0.075406, 51.5055]),
properties: {
'title': 'Tower Bridge',
'city': 'London',
'built': 1886,
},
),
]);

Using GeoJSON in your Dart app

GeoJSON is a textual data format for encoding geometry and feature objects introduced above. The format is specified by the RFC 7946 standard. Geometry objects use WGS 84 longitude/latitude geographic coordinates.

This format is fully supported by the geobase package that let Dart applications decode and encode GeoJSON compliant data.

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));

Optimize GeoJSON encoding with newline-delimited data

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.

Or by the way the same thing is also known as GeoJSON Lines (GeoJSONL) or GeoJSON Text Sequences (formalized by RFC 8142).

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.

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.

Well-known text representation of geometry (WKT)

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);

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);

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

Well-known binary of geometry (WKB)

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.

And along the standard specification there exists also a PostGIS-specific format flavor called Extended WKB (or EWKB). It supports encoding also coordinate reference system (SRID) information, and flags for extra coordinates (Z and M) are encoded differently to the standard 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);
}
}

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

Summary

The geobase package as a part of Geospatial tools for Dart 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)

You can follow development of these tools in GitHub (see milestones and issues). Comments, suggestions and any contributions are also welcome!

And in case you want to know more about Geospatial tools for Dart, you should also check a more comprehensive article (October 2023).

--

--