navibyte / geospatial Goto Github PK
View Code? Open in Web Editor NEWGeospatial data structures, tools and utilities for Dart and Flutter.
License: Other
Geospatial data structures, tools and utilities for Dart and Flutter.
License: Other
Immutable Point classes has constructors, members and getters like this, for example Point2
:
/// A point at given [x] and [y].
const Point2({required num x, required num y})
: _x = x,
_y = y;
final num _x, _y;
@override
num get x => _x;
@override
num get y => _y;
Streamline code a bit be refactoring these like this:
/// A point at given [x] and [y].
const Point2({required this.x, required this.y});
@override
final num x;
@override
final num y;
UriResolver defined (already previously too)
/// A function to resolve an absolute URI from an URI [reference].
///
/// Throws [ClientException] if the given [reference] is not allowed
/// according to security policies of a resolver, or if it's not resolvable.
typedef UriResolver = Uri Function(Uri reference);
UriResolver was moved to => lib/src/api/address
folder
A new interface defined also on some code folder:
/// An anchor for a resource.
abstract class Anchor {
const Anchor();
/// The URI [reference] to a resource this anchor is referring.
Uri get reference;
}
Then Content definition was changed to implement Anchor too:
abstract class Content extends Head implements Anchor, Body
All definitions mentioned available via package:datatools/base_api.dart
The geocore
package (and also geodata
) has dependency to the attributes
package via following imports:
import 'package:attributes/entity.dart';
import 'package:attributes/values.dart';
It's used only for following things:
Feature
extends Entity
from the attributes
packageid
member of Entity
is of the type Identifider
properties
member of Entity
is of the type DataObject
References are located in following source code:
lib/src/feature/feature.dart
lib/src/parse/geojson/geojson.dart
Consider removing the dependency to the attributes
package and use some more generic types instead.
Properties could be handled as Map<String, Object?>
and id stored as String (even if both number and text representations could occur for feature ids in GeoJSON for example).
This change would allow more generic usage of the geocore
library and Feature
classes it defines without introducing a dependency to a single package (that is now used only very partially).
Any side effects from this change?
How to check also the geodata
package.
Background
Bob Nyström (2013):
When parsing data from some external data source, like WKT or GeoJSON data, in the scope of this library, such external data might have invalid data that parser code must handle. When encountering invalid data, and if a parser cannot handle such invalid data, it might be better to throw an exception (like FormatException in Dart) rather than throwing an error. Invalid data from an external data source or file are not programmatic errors.
Some code in geocore still might throw errors when reading (and cannot handle) invalid data. So must check and correct such occurences if found.
New constructors for Point
concrete classes like, here a sample for Point2
, other classes in similar ways:
/// A point parsed from [text] with coordinates in order: x, y.
///
/// If [parser] is null, then WKT [text] like "10.0 20.0" is expected.
///
/// Throws FormatException if cannot parse.
factory Point2.parse(String text, {ParseCoords? parser});
/// A point parsed from [text] with coordinates in order: x, y.
///
/// If [parser] is null, then WKT [text] like "10.0 20.0" is expected.
///
/// Returns null if cannot parse.
static Point2? tryParse(String text, {ParseCoords? parser});
Also more complex geometries new ways to construct, for example LineString
:
/// Create [LineString] from [values] with a chain of points.
///
/// An optional [bounds] can be provided or it's lazy calculated if null.
factory LineString.make(
Iterable<Iterable<num>> values, PointFactory<T> pointFactory,
{LineStringType type = LineStringType.any, Bounds? bounds});
/// Create [LineString] parsed from [text] with a chain of points.
///
/// If [parser] is null, then WKT [text] like "25.1 53.1, 25.2 53.2" is
/// expected.
///
/// Throws FormatException if cannot parse.
factory LineString.parse(String text, PointFactory<T> pointFactory,
{LineStringType type = LineStringType.any,
ParseCoordsList? parser});
Other geometries in similar ways.
See description from the dataflow
repository:
Apply very_good_analysis 2.4.0+ lint rules #17
Same thing here.
Now we have support for creating "empty geometries":
Point.empty()
Geometry.empty()
But no direct way to create "empty geometries" for other geometry types like LineString, Polygon, etc.
What are empty geometries anyway?
Geospatial formats like WKT support this kind of stuff:
POINT EMPTY
MULTIPOLYGON EMPTY
Add full support to all geometry types and WKT parser at least.
Smaller fixes for the geocore
BETA version 0.6.2:
make
and parse
factory methodsCRS
) identifiersInitial support for WKT was in BETA version 0.6.1 - see #22.
Add also support for GEOMETRYCOLLECTION.
May need some refactoring on already implemented parsers.
Currently specified as private helper class on package:geocore/base.dart
:
/// A helper class to calculate [bounds] for a set of points and other bounds.
///
/// Use [addPoint] and [addBounds] methods to add geometries to be used on
/// calculation. A value for calculations can be obtained from [bounds].
class _BoundsBuilder {
...
As this is utility class, move from "base" to under "lib/src/utils/bounds" as public class, but not exported.
Not many tests yet...
Classes implementing Point
has static const factories named geometry
:
/// A [PointFactory] creating [Point2] instances.
static const PointFactory<Point2> geometry =
CastingPointFactory<Point2>(Point2.origin());
Usage like this:
LineString.parse('100.0 200.0, 400.0 500.0', Point2.geometry);
LineString.parse('100.0 200.0 300.0 23.4, 400.0 500.0 600.0 54.8', Point3m.geometry);
Consider renaming geometry
to coordinates
, that would make usage more descriptive, maybe?
Usage would be like this:
LineString.parse('100.0 200.0, 400.0 500.0', Point2.coordinates);
LineString.parse('100.0 200.0 300.0 23.4, 400.0 500.0 600.0 54.8', Point3m.coordinates);
Factories named geometry
would be kept for some time, but deprecated.
Goal: Enhance methods of Point returning coordinate values as String
Need from entity classes with point geometries - making json element serialization easier.
Point
and Bounds
classes, new optional parameter fractionDigits
, see method definitions updated:
/// Writes coordinate values to [buffer] delimited by [delimiter].
///
/// Use [fractionDigits] to set a number of decimals to nums with decimals.
void writeValues(
StringSink buffer, {
String delimiter = ',',
int? fractionDigits,
});
/// Returns coordinate values as a string delimimited by [delimiter].
///
/// Use [fractionDigits] to set a number of decimals to nums with decimals.
String valuesAsString({
String delimiter = ',',
int? fractionDigits,
});
Point
class, new method:
/// Returns WKT coords (ie. "35 10" for a point with x=35 and y=10).
///
/// Use [fractionDigits] to set a number of decimals to nums with decimals.
String toWktCoords({int? fractionDigits}) =>
valuesAsString(delimiter: ' ', fractionDigits: fractionDigits);
Also internal utility function:
/// Uses `String.toStringAsFixed()` when [n] contains decimals.
///
/// For example returns '15' if a double value is 15.00, and '15.50' if a double
/// value is 15.50.
///
/// See: https://stackoverflow.com/questions/39958472/dart-numberformat
String toStringAsFixedWhenDecimals(num n, int fractionDigits) =>
n.toStringAsFixed(n.truncateToDouble() == n ? 0 : fractionDigits);
Geocore package has support for GeoJSON parser.
Geodata package provides client side access to different resources. Currently only partial support for OGC API Features (see #9).
Implement also support for accessing a web or file resource hosting GeoJSON file to geodata package.
Need also "client" implementation for a file resource to datatools package + other enchancements maybe too.
Adapt to #16 and #17 enhancements.
New planned class hierarchy
ResourceMeta
class with with descriptor
and links
properties
ProviderMeta
class for metadata about API provider, with conformance
and collections
properties tooCollectionMeta
class for metadata about a geospatial collection under a provider, with id
and extent
prorperty tooCurrently just throws an exception when calling async fetching functions to read data from an external data source.
Consider is this fine, or some other mechanism. Or specific exception clasess.
GeoJSON parser supports constructing Feature objects containing id, properties and geometry as described on GeoJSON spec at RFC 7946.
However GeoJSON spec allows foreign members like on snippets extracted from the spec below:
Here "title" is foreign member:
{
"type": "Feature",
"id": "f1",
"geometry": {...},
"properties": {...},
"title": "Example Feature"
}
And here "centerline" is foreign member and NOT a GeoJSON geometry
{
"type": "Feature",
"id": "f2",
"geometry": {...},
"properties": {...},
"centerline": {
"type": "LineString",
"coordinates": [
[-170, 10],
[170, 11]
]
}
}
GeoJSON parser should be refactored to allow parser clients to read also foreign member data.
DRAFT for some future version (0.8 or 0.9 maybe).
Planned structure for restructuring web api client for geospatial data, something like this
Services do not have common abstractions, as different services differ so much starting from standard specific stuff.
However FeatureStore
above is meant to be common abstraction, letting accessing features (or other geospatial data) from different service types with shared interface. Services just provide async methods to access remote and local resources. Stores uses services, and may combine different services, and may cache data, and has mechanism to notify listeners.
LinksMeta
specified on geodata
package.
Generalize and move to datatools
package.
Relations from IANA specs:
Changes on ValueAccessor:
package:attributes/values.dart
Counted
any more
bool existsNull(K key);
, was previously hasNull
bool existsNonNull(K key);
num getNum(K key, {num? min, num? max});
num
type at key
.getInt
and getDouble
properties available too.getNum
when accessor might have either int or double property value.num? tryNum(K key, {num? min, num? max});
DateTime getTimeUTC(K key, {DateTime Function(Object?)? parse});
parse
to define app specific conversion.DateTime? tryTimeUTC(K key, {DateTime Function(Object?)? parse});
parse
to define app specific conversion.Changes on ValueAccessorMixin
package:attributes/values.dart
Exception
not only FormatException
Example of ValueAccessorMixin method:
@override
String? tryString(K key) {
try {
return getString(key);
} on Exception {
return null;
}
}
New base interface for property collections (maps and lists)
abstract class Properties<K> extends ValueAccessor<K> implements Counted {
PropertyMap getMap(K key);
PropertyMap? tryMap(K key);
PropertyList getList(K key);
PropertyList? tryList(K key);
}
Changes on PropertyMap:
ValueAccessor<String>
directlyProperties<String>
PropertyMap.from(Map<String, dynamic> source)
Also new PropertyList that extends Properties<int>
New member to be defined on Point
abstract class and implemented in sub classes:
/// Copies this point with the compatible type and sets given coordinates.
///
/// Optional [x], [y], [z] and [m] values, when given, override values of
/// this point object. If the type of this point does not have a certain
/// value, then it's ignored.
Point copyWith({num? x, num? y, num? z, num? m});
Same definition on GeoPoint
that extends Point
:
/// Copies this point with the compatible type and sets given coordinates.
///
/// Optional [x], [y], [z] and [m] values, when given, override values of
/// this point object. If the type of this point does not have a certain
/// value, then it's ignored.
///
/// Properties have equality (in context of this library): [lon] == [x],
/// [lat] == [y], [elev] == [z]
@override
GeoPoint copyWith({num? x, num? y, num? z, num? m});
PointSeries
has property to check whether is closed:
/// True if the first and last point equals in 2D.
bool get isClosed;
Add also a method to check within a tolerance:
/// True if the first and last point equals in 2D within [toleranceHoriz].
bool isClosedBy(num toleranceHoriz);
Updated documentation
geocore
package README (at https://github.com/navibyte/geospatial/tree/main/dart/geocore)Illustrative geographic images, sourcing from Wikimedia commons, collected to a separate repository: https://github.com/navibyte/geospatial_docs
geocore_example.dart updated with samples used also on README
Guidelines: Effective Dart
To be considered:
https://github.com/tenhobi/effective_dart
See description Apply very_good_analysis 2.3.0+ lint rules in the dataflow
repository.
Plan updated 2024-04-16
Implementation for v.1.1.0
Factory constructors on classes implementing Point
are of form:
factory Point2.from(Iterable<double> coords)
See #2 and #3 for other issues about coordinate values and methods creating new instances.
Change factory constructors also similary to new signature:
factory Point2.from(Iterable<num> coords, {int? offset});
Same as Official Dart lint rules applied with recommend set #2 at the dataflow
repository.
Some mini libraries should also export some of base classes.
For example if needing GeoJSON parser but also base geometries, you have to import:
import 'package:geocore/base.dart';
import 'package:geocore/parse_geojson.dart';
Maybe it would be handy if import for parse_geojson.dart
would export also base geometries (and factory abstractions).
The package contains following mini-libraries:
Library | Description |
---|---|
base | Geometry classes including points, bounds, line strings, polygons and more. |
crs | Classes to represent coordinate reference system (CRS) identifiers. |
feature | Feature and FeatureCollection to handle dynamic geospatial data objects. |
geo | Geographic points and bounds classes to represent longitude-latitude data |
meta_extent | Metadata structures to handle extents with spatial and temporal parts. |
parse_factory | Base interfaces and implementations for geospatial data factories. |
parse_geojson | Geospatial data factory to parse GeoJSON from text strings. |
parse_wkt | Geospatial data factory to parse WKT from text strings. |
For example to access a mini library you should use an import like:
import 'package:geocore/base.dart';
To use all libraries of the package:
import 'package:geocore/geocore.dart';
Optimize data structures and class usage.
Consistency on constructors, factories and methods.
Check also Dart 2.13 type aliases for possibilities to clean up generic class definitions.
Might have use cases if GeoJSON parser would have a filtering mechanism of some kind - parse only those features or geometries that match with filter.
GeoPoint2m
, GeoPoint3
and GeoPoint3m
geographical points has either elev
or m
coordinate values, or both. Default constructor requires also these, for example:
const GeoPoint3m({
required double lon,
required double lat,
required double elev,
required this.m,
}) : super(lon: lon, lat: lat, elev: elev);
To be consistent with Point2m
, Point3
and Point3m
change definition so that elev
and m
has default 0.0 value on default constructors.
const GeoPoint3m({
required double lon,
required double lat,
double elev = 0.0,
this.m = 0.0,
}
PointSeries
and BoundedSeries
have methods, whose definition:
/// Returns a new lazy series where items intersects with [bounds].
///
/// Even if an item on this series has a complex geometry, only bounds
/// of that geometry is tested (intersection) with the given [bounds].
///
/// Those items that has empty bounds are not matched.
S intersectByBounds(Bounds bounds);
/// Returns a new lazy series where items intersects with [bounds] in 2D.
///
/// Even if an item on this series has a complex geometry, only bounds
/// of that geometry is tested (intersection) with the given [bounds].
///
/// Those items that has empty bounds are not matched.
S intersectByBounds2D(Bounds bounds);
Change their definition (and also implementations):
/// Returns a new series where items intersects with [bounds].
///
/// The intersected series is populated by default. If [lazy] is set true then
/// returns a new lazy series with items intersected lazily.
///
/// Even if an item on this series has a complex geometry, only bounds
/// of that geometry is tested (intersection) with the given [bounds].
///
/// Those items that has empty bounds are not matched.
S intersectByBounds(Bounds bounds, {bool lazy = false});
/// Returns a new series where items intersects with [bounds] in 2D.
///
/// The intersected series is populated by default. If [lazy] is set true then
/// returns a new lazy series with items intersected lazily.
///
/// Even if an item on this series has a complex geometry, only bounds
/// of that geometry is tested (intersection) with the given [bounds].
///
/// Those items that has empty bounds are not matched.
S intersectByBounds2D(Bounds bounds, {bool lazy = false});
For example Point2
:
/// A point parsed from [text] with coordinates given in order: x, y.
///
/// Coordinate values in [text] are separated by [delimiter]. For example
/// `Point2.fromText('10.0;20.0', delimiter: ';')` returns the same point as
/// `Point2.xy(10.0, 20.0)`.
///
/// If [delimiter] is not provided, values are separated by whitespace.
///
/// Throws FormatException if cannot parse.
factory Point2.fromText(
String text, {
Pattern? delimiter,
});
Also similar methods to other Point classes.
Otherway, new method added to Point
abstract base class:
/// Returns coordinate values as text separated by [delimiter].
///
/// If [delimiter] is not provided, values are separated by whitespace. For
/// example "10.1 20.2" is returned for a point with x=10.1 and y=20.2.
///
/// Use [fractionDigits] to set a number of decimals to nums with decimals.
String toText({
String delimiter = ' ',
int? fractionDigits,
});
Deprecating this on Point
abstract class:
/// Returns WKT coords (ie. "35 10" for a point with x=35 and y=10).
///
/// Use [fractionDigits] to set a number of decimals to nums with decimals.
@Deprecated('Use toText instead')
String toWktCoords({int? fractionDigits});
Some helper methods needed on package internal utilities.
Currently we have for Point
class:
/// True if this point equals with [other] point in 2D.
bool equals2D(Point other) =>
isNotEmpty && other.isNotEmpty && x == other.x && y == other.y;
/// True if this point equals with [other] point in 3D.
bool equals3D(Point other) => equals2D(other) && z == other.z;
Add optional tolerance argument to have:
/// True if this point equals with [other] point in 2D by testing x and y.
///
/// If [toleranceHoriz] is given, then differences on x and y coordinate
/// values between this and [other] must be <= tolerance. Otherwise value
/// must be exactly same.
///
/// Tolerance values must be null or positive (>= 0).
bool equals2D(Point other, {num? toleranceHoriz}) {
assert(
toleranceHoriz == null || toleranceHoriz >= 0.0,
'Tolerance must be null or positive (>= 0)',
);
if (isEmpty || other.isEmpty) {
return false;
}
return toleranceHoriz != null
? (x - other.x).abs() <= toleranceHoriz &&
(y - other.y).abs() <= toleranceHoriz
: x == other.x && y == other.y;
}
/// True if this point equals with [other] point in 3D by testing x, y and z.
///
/// If [toleranceHoriz] is given, then differences on x and y coordinate
/// values between this and [other] must be <= tolerance. Otherwise value
/// must be exactly same.
///
/// The tolerance for z coordinate values is given by an optional
/// [toleranceVert] value.
///
/// Tolerance values must be null or positive (>= 0).
bool equals3D(Point other, {num? toleranceHoriz, num? toleranceVert}) {
assert(
toleranceVert == null || toleranceVert >= 0.0,
'Tolerance must be null or positive (>= 0)',
);
if (!equals2D(other, toleranceHoriz: toleranceHoriz)) {
return false;
}
return toleranceVert != null
? (z - other.z).abs() <= toleranceVert
: z == other.z;
}
New package or sub library definition:
/// Generic API abstractions (addresses, content, control data, exceptions).
///
/// Usage: import `package:datatools/base_api.dart`
library base_api;
export 'src/api/address.dart';
export 'src/api/content.dart';
export 'src/api/control.dart';
export 'src/api/exceptions.dart';
The previously available package package:datatools/fetch_api.dart
is now defined as
/// Fetch API abstraction (addresses, content, control data, exceptions, fetch).
///
/// Usage: import `package:datatools/fetch_api.dart`
library fetch_api;
export 'base_api.dart';
export 'src/api/fetch.dart';
Point has following method to create new points of the same sub class point type with given values:
Point newPoint({double x = 0.0, double y = 0.0, double z = 0.0, double m = 0.0});
This has issues
int
as param, need to change params to num
, see #2Change to:
Point newWith({num x = 0.0, num y = 0.0, num? z, num? m});
Also another version of point factory letting reading from coord array:
Point newFrom(Iterable<num> coords, {int? offset, int? length});
The latter could be generalized:
abstract class CoordinateFactory<T extends Geometry> {
T newFrom(Iterable<num> coords, {int? offset, int? length})
}
https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
Now (at least some level of) support for:
However NO support for these, and not yet even planned any, is there need?
Well-known binary (WKB) seems to have even more, but maybe most of these are totally out of focus for the geocore
library...
Most immutable classes uses EquatableMixin
from equatable package that helps to implement equality without needing to explicitly override == and hashCode
However for example points has common interface Point and multiple implementing classes like (Point2, Point3 etc.). Need some consideration how ==
and hashCode
should work on these classes. Same issue with other classes extending Geometry.
Currently no support at all.
Need some specification what's really needed or not.
There are good libraries for coordinate transformations, at least following, maybe others too.
proj4dart by maRci002:
A Dart library to transform point coordinates from one coordinate system to another, including datum transformations
Support creating geometry classes from POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING and MULTIPOLYGON types.
The abstract Point
class following factory methods to create new point instances of the same sub type methods are called on.
Point newWith({num x = 0.0, num y = 0.0, num? z, num? m});
Point newFrom(Iterable<num> coords, {int? offset, int? length});
Sub classes like Point2
implement these methods like:
@override
Point newWith({num x = 0.0, num y = 0.0, num? z, num? m}) =>
Point2(x: x, y: y);
@override
Point newFrom(Iterable<num> coords, {int? offset, int? length}) {
CoordinateFactory.checkCoords(2, coords, offset: offset, length: length);
return Point2.from(coords, offset: offset);
}
This should work fine.
However if you are using a specific Point
sub type, like Point2
, then it might be useful to define more specific return type.
That is, for Point2
the implementation would change to following:
@override
Point2 newWith({num x = 0.0, num y = 0.0, num? z, num? m}) =>
Point2(x: x, y: y);
@override
Point2 newFrom(Iterable<num> coords, {int? offset, int? length}) {
CoordinateFactory.checkCoords(2, coords, offset: offset, length: length);
return Point2.from(coords, offset: offset);
}
Sample from current code at src/geo/geobounds.dart
:
import 'package:equatable/equatable.dart';
import '../base.dart';
import 'geopoint.dart';
This has three kind of imports:
Relative imports are short and useful, but relative imports from other folders could some time mean imports like ../../subpackage/otherclass.dart
where multiple ..
can make it difficult to see directly what folder offers a particular class.
Convention to be applied:
package:
/src
The sample shall change:
import 'package:equatable/equatable.dart';
import '/src/base.dart';
import 'geopoint.dart';
OGC API Features client is implemented by OGCAPIFeatures
class.
Support for the OGC API Features standard, parts 1 + 2, is not yet complete.
Complete support with some tests included.
About standard:
https://ogcapi.ogc.org/features/
https://github.com/opengeospatial/ogcapi-features
Point
-interface has x, y, z and m getter properties returning double
.
Most implementing classes use also double
as storage.
However there Point2i
and Point3i
use int
as storage. As x, y, z and m getter properties are used, int
must be returned as double
.
Feature change: getters return num
which can be either int
or double
.
See issue Add Fetch API binding and support for in-memory data storage #16 at the Dataflow tools for Dart for the datatools packages.
When this issue get implemented, then utilise it also in the geodata package to provide mechanism to read geospatial features from some in-memory storage along with Web APIs and file storages already supported.
For example The Atom Syndication format by RFC 4287 has descriptive fields, explained by Introduction to Atom
feed
element
title
: Contains a human readable title for the feed. Often the same as the title of the associated website.subTitle
: Contains a human-readable description or subtitle for the feed.entry
element
title
: Contains a human readable title for the entry.summary
: Conveys a short summary, abstract, or excerpt of the entry.Atom has many other fields too for category, author, ids, links etc, but they are not generic descriptive elements.
Suggested generic metadata item for describing some item on this package:
Descriptor
class
title
(required): A human-readable title or header for an itemsubTitle
(optional): A human-readable subtitle, sub header or description for an itemsummary
(optional): A short summary or abstract for an itemA declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.