As already mentioned
Geospatial tools for Dart provides two stable Dart packages:
🌐 geobase : Geospatial data structures (coordinates, geometries, features, metadata), spherical geodesy, projections and tiling schemes. Vector data format support for GeoJSON , WKT and WKB .
🌎 geodata : Geospatial feature service Web APIs with support for GeoJSON and OGC API Features clients.
This chapter introduces some of the key capabilities how these packages help
Dart and
Flutter developers to build their own
geospatial-powered applications.
🌐 Coordinates (with geobase )
General purpose positions, series of positions and bounding boxes:
// A position as a view on a coordinate array containing x and y.
Position . view ([ 708221.0 , 5707225.0 ]);
// The sample above shorted.
[ 708221.0 , 5707225.0 ].xy;
Box . view ([ 70800.0 , 5707200.0 , 70900.0 , 5707300.0 ]);
// A series of positions from an array of position objects.
[ 70800.0 , 5707200.0 ].xy, // position 0 with (x, y) coordinate values
[ 70850.0 , 5707250.0 ].xy, // position 1 with (x, y) coordinate values
[ 70900.0 , 5707300.0 ].xy, // position 2 with (x, y) coordinate values
Geographic and projected positions and bounding boxes:
// A geographic position without and with an elevation.
Geographic (lon : - 0.0014 , lat : 51.4778 );
Geographic (lon : - 0.0014 , lat : 51.4778 , elev : 45.0 );
// A projected position without and with z.
Projected (x : 708221.0 , y : 5707225.0 );
Projected (x : 708221.0 , y : 5707225.0 , z : 45.0 );
// Geographic and projected bounding boxes.
GeoBox (west : - 20 , south : 50 , east : 20 , north : 60 );
GeoBox (west : - 20 , south : 50 , minElev : 100 , east : 20 , north : 60 , maxElev : 200 );
ProjBox (minX : 10 , minY : 10 , maxX : 20 , maxY : 20 );
// Positions and bounding boxes can be also built from an array or parsed.
Geographic . build ([ - 0.0014 , 51.4778 ]);
Geographic . parse ( '-0.0014,51.4778' );
Geographic . parse ( '-0.0014 51.4778' , delimiter : ' ' );
Geographic . parseDms (lon : '0° 00′ 05″ W' , lat : '51° 28′ 40″ N' );
GeoBox . build ([ - 20 , 50 , 100 , 20 , 60 , 200 ]);
GeoBox . parse ( '-20,50,100,20,60,200' );
GeoBox . parseDms (west : '20°W' , south : '50°N' , east : '20°E' , north : '60°N' );
Read more about coordinates on the
documentation.
🧩 Simple geometries (with geobase )
As a quick sample, this is how geometry objects with 2D coordinate are created
using geobase :
Geometry Shape Dart code to build objects Point Point.build([30.0, 10.0])
LineString LineString.build([30, 10, 10, 30, 40, 40])
Polygon Polygon.build([[30, 10, 40, 40, 20, 40, 10, 20, 30, 10]])
Polygon (with a hole) Polygon.build([[35, 10, 45, 45, 15, 40, 10, 20, 35, 10], [20, 30, 35, 35, 30, 20, 20, 30]])
MultiPoint MultiPoint.build([[10, 40], [40, 30], [20, 20], [30, 10]])
MultiLineString MultiLineString.build([[10, 10, 20, 20, 10, 40], [40, 40, 30, 30, 40, 20, 30, 10]])
MultiPolygon MultiPolygon.build([[[30, 20, 45, 40, 10, 40, 30, 20]], [[15, 5, 40, 10, 10, 20, 5, 10, 15, 5]]])
MultiPolygon (with a hole) MultiPolygon.build([[[40, 40, 20, 45, 45, 30, 40, 40]], [[20, 35, 10, 30, 10, 10, 30, 5, 45, 20, 20, 35], [30, 20, 20, 15, 20, 25, 30, 20]]])
GeometryCollection GeometryCollection([Point.build([30.0, 10.0]), LineString.build([10, 10, 20, 20, 10, 40]), Polygon.build([[40, 40, 20, 45, 45, 30, 40, 40]])])
Primitive geometries introduced above contain geographic or projected positions:
Point
with a single position
LineString
with a chain of positions (at least two positions)
Polygon
with an array of linear rings (exactly one exterior and 0 to N interior rings with each ring being a closed chain of positions)
In previous samples position data (chains of positions) is NOT modeled as
iterables of position objects, but as a flat structure represented by arrays of
coordinate values, for example:
2D position arrays: [x0, y0, x1, y1, x2, y2, ...]
3D position arrays: [x0, y0, z0, x1, y1, z1, x2, y2, z2, ...]
To distinguish between arrays of different spatial dimensions you can use
Coords
enum:
LineString . build ([ 30 , 10 , 10 , 30 , 40 , 40 ]); // default type == Coords.xy
LineString . build ([ 30 , 10 , 10 , 30 , 40 , 40 ], type : Coords .xy);
LineString . build ([ 30 , 10 , 5.5 , 10 , 30 , 5.5 , 40 , 40 , 5.5 ], type : Coords .xyz);
Read more about simple geometries on the
documentation.
🔷 Geospatial features (with geobase )
Features represent geospatial entities with properties and geometries:
// a point geometry with a position (lon, lat, elev)
geometry : Point . build ([ - 0.0014 , 51.4778 , 45.0 ]),
'title' : 'Royal Observatory' ,
The GeoJSON format is supported as text input and output for features:
final feature = Feature . parse (
"coordinates": [-0.0014, 51.4778, 45.0]
"title": "Royal Observatory"
print (feature. toText (format : GeoJSON .feature));
Geospatial feature collections can be instantiated easily too:
// A geospatial feature collection (with two features):
// a point geometry with a position (lon, lat, elev)
geometry : Point . build ([ - 0.0014 , 51.4778 , 45.0 ]),
'title' : 'Royal Observatory' ,
// a point geometry with a position (lon, lat)
geometry : Point . build ([ - 0.075406 , 51.5055 ]),
Read more about geospatial features on the
documentation.
As already introduced the geobase
package supports
GeoJSON ,
WKT
and
WKB vector
data formats.
Decoding and encoding in details
GeoJSON, WKT and WKB formats are supported as input and output:
// Parse a geometry from GeoJSON text.
final geometry = LineString . parse (
'{"type": "LineString", "coordinates": [[30,10],[10,30],[40,40]]}' ,
format : GeoJSON .geometry,
// Encode a geometry as GeoJSON text.
print (geometry. toText (format : GeoJSON .geometry));
// Encode a geometry as WKT text.
print (geometry. toText (format : WKT .geometry));
// Encode a geometry as WKB bytes.
final bytes = geometry. toBytes (format : WKB .geometry);
// Decode a geometry from WKB bytes.
LineString . decode (bytes, format : WKB .geometry);
A sample showing more deeply how to handle WKB and 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);
// encode as Extended WKB data (format: `WKB.geometryExtended`), prints:
// 0101000080c7bab88d06f056bfb003e78c28bd49400000000000804640
final ewkbHex = p. toBytesHex (format : WKB .geometryExtended);
// 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
p. toBytesHex (format : WKB .geometryExtended, crs : CoordRefSys . EPSG_4326 );
// 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);
print ( 'SRID from EWKB data: $ srid ' );
// after finding out CRS, an actual point can be decoded
// Point.decodeHex(ewkbHexWithSRID, format: WKB.geometry);
Using Newline-delimited GeoJSON (or “GeoJSONL”) is as easy as using the
standard GeoJSON:
/// a feature collection encoded as GeoJSONL and containing two features that
/// are delimited by the newline character \n
{"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 ));
Read more about vector data formats on the
documentation.
Temporal instants and intervals, and geospatial extents:
// An instant and three intervals (open-started, open-ended, closed).
Instant . parse ( '2020-10-31 09:30Z' );
Interval . parse ( '../2020-10-31' );
Interval . parse ( '2020-10-01/..' );
Interval . parse ( '2020-10-01/2020-10-31' );
// An extent with spatial (WGS 84 longitude-latitude) and temporal parts.
bbox : GeoBox (west : - 20.0 , south : 50.0 , east : 20.0 , north : 60.0 ),
interval : Interval . parse ( '../2020-10-31' ),
Read more about metadata on the
documentation.
📐 Spherical geodesy (with geobase )
Spherical geodesy functions for great circle (shown below) and rhumb line
paths:
final greenwich = Geographic . parseDms (lat : '51°28′40″ N' , lon : '0°00′05″ W' );
final sydney = Geographic . parseDms (lat : '33.8688° S' , lon : '151.2093° E' );
greenwich.spherical. distanceTo (sydney);
// Initial and final bearing: 61° -> 139°
greenwich.spherical. initialBearingTo (sydney);
greenwich.spherical. finalBearingTo (sydney);
// Destination point (10 km to bearing 61°): 51° 31.3′ N, 0° 07.5′ E
greenwich.spherical. destinationPoint (distance : 10000 , bearing : 61.0 );
// Midpoint: 28° 34.0′ N, 104° 41.6′ E
greenwich.spherical. midPointTo (sydney);
Read more about spherical geodesy
on the documentation.
ℹ Other capabilities (geobase )
Coordinate projections, tiling schemes (web mercator, global geodetic) and
coordinate array classes are some of the more advanced topics not introduced
here. Please see separate chapters about
geometry calculations ,
projections ,
tiling schemes and
coordinate arrays to learn
about them.
🌎 Web APIs for GeoJSON (with geodata )
The geodata package has the following
diagram describing a decision flowchart how to select a client class to access
GeoJSON features:
Quick start code to access a Web API service conforming to OGC API Features:
// 1. Get a client instance for a Web API endpoint.
final client = OGCAPIFeatures . http (endpoint : Uri . parse ( '...' ));
// 2. Access/check metadata (meta, OpenAPI, conformance, collections) as needed.
final conformance = await client. conformance ();
if ( ! conformance. conformsToFeaturesCore (geoJSON : true )) {
return ; // not conforming to core and GeoJSON - so return
// 3. Get a feature source for a specific collection.
final source = await client. collection ( 'my_collection' );
// 4. Access (and check) metadata for this collection.
final meta = await source. meta ();
print ( 'Collection title: ${ meta . title } ' );
// 5. Access feature items.
final items = await source. itemsAll (limit : 100 );
// 6. Check response metadata.
print ( 'Timestamp: ${ items . timeStamp } ' );
// 7. Get an iterable of feature objects.
final features = items.collection.features;
// 8. Loop through features (each with id, properties and geometry)
for ( final feat in features) {
print ( 'Feature ${ feat . id } with geometry: ${ feat . geometry } ' );
Read more about GeoJSON client and
OGC API Features client on the
documentation.
🚀 Demos and samples
✨ See also the
Geospatial demos for Dart code
repository for demo and sample apps demonstrating the usage of
geobase and
geodata packages along with other topics.