minted_geography
Coordinates, geohashes and bounding boxes as well-modelled value types.
Part of the minted family: pure-Dart value types built on
parse, don't validate, so the parser is the only door in and anything that came through it is
well-formed by construction. Once you hold a GeoCoordinate, both halves are in range and in the
order you meant, and once you hold a Geohash, it decodes.
Install
dart pub add minted_geography
minted comes with it, holding the vocabulary a parse hands back
(ParseOutcome, MintedFailure), and so does
minted_constraints for the primitives this package's
getters return. Nothing here drags in
another domain's engine.
What's in the box
| Type | What it guarantees | Standard |
|---|---|---|
GeoCoordinate |
a bounded latitude and longitude. All 3 ISO 6709 widths read as degrees | ISO 6709 |
Geohash |
a base32 cell, not a point. The 4 letters base32 drops are refused | CTA-5009-A |
GeoBounds |
a box that may cross the antimeridian, which west <= east would refuse |
RFC 7946 §5 |
A swapped latitude and longitude is a type bug no range check catches, so the pair is named at the boundary. It's a surface coordinate: altitude and a CRS identifier are refused rather than silently dropped, since their sign, units and datum are all defined by the CRS.
A geohash is a cell, not a point, which a String cannot say: bounds is that cell and centre
one point in it. toLowerCase() isn't validation either, the alphabet having dropped a, i, l
and o.
west > east is not a transposition, it is how RFC 7946 §5.2 writes a box across the antimeridian,
so GeoBounds reports the crossing and contains honours it rather than every caller re-deriving
the case that goes wrong near ±180.
Latitude and Longitude carry the ranges, so the assembly doors prevent an impossible degree
instead of reporting one: GeoCoordinate.from takes them and cannot fail, tryFrom takes raw
numbers. Both implement double, so reading a degree needs no unwrapping and a box composes
straight out of a coordinate's parts.
A quick taste
final eiffel = GeoCoordinate.tryParse('+48.8577+002.295/')!;
eiffel.latitude; // 48.8577
eiffel.iso6709; // '+48.8577+002.295/' (canonical form)
eiffel.sexagesimal; // '48°51′27.72″N 2°17′42″E' (display form)
// ISO 6709 selects the unit by field width, and all three widths fold to degrees, so the same
// point spelled as degrees-minutes-seconds is the same value:
GeoCoordinate.tryParse('+485127.72+0021742/') == eiffel; // true
GeoCoordinate.tryParse('+5012-00010/')!.latitude; // 50.2 (degrees and minutes)
GeoCoordinate.tryParse('+46+2/'); // null: an unpadded longitude is a different location
// named and typed, so it can't be written swapped. from cannot fail, tryFrom takes raw numbers:
GeoCoordinate.from(latitude: Latitude.tryFrom(48.8577)!, longitude: Longitude.tryFrom(2.295)!);
GeoCoordinate.tryFrom(latitude: 48.8577, longitude: 2.295);
// Geohash: from takes a coordinate and cannot fail, so it hands back the value, not an outcome.
final cell = Geohash.from(coordinate: eiffel, precision: NaturalNumber.tryFrom(5)!);
cell.value; // 'u09tu' (canonical form: trimmed, lower-cased)
cell.centre.iso6709; // '+48.84521484375+002.30712890625/' inside the cell, not the tower
cell.bounds.bbox; // '2.28515625,48.8232421875,2.3291015625,48.8671875' the cell itself
Geohash.tryParse('EZS42 ') == Geohash.tryParse('ezs42'); // true: case and padding fold away
Geohash.tryParse('ezsa2'); // null: 'a' is not in the geohash alphabet
['ezs42', 'ezs41', 'u4pruy']..sort(); // spatial order free: the alphabet is ASCII-ascending
// GeoBounds: west past east is a crossing, not a transposition.
final fiji = GeoBounds.tryParse('170,-45,-170,-35')!;
fiji.crossesAntimeridian; // true
fiji.contains(GeoCoordinate.tryFrom(latitude: -40, longitude: 179)!); // true
fiji.contains(GeoCoordinate.tryFrom(latitude: -40, longitude: 0)!); // false, the long way round
GeoBounds.tryParse('[-180, -90, 180, 90]')?.bbox; // '-180.0,-90.0,180.0,90.0' the whole world
The runnable version is the example.
Named constants
A type's named values live in a companion <Type>Constants namespace, so the type itself stays its
parsing API. They're const, so they reach where a tryParse(...)! can't.
GeoBoundsConstants.wholeWorld.bbox; // '-180.0,-90.0,180.0,90.0'
LatitudeConstants.max; // 90.0, the last degree that parses
GeohashConstants.first.value; // '0', which every geohash sorts at or after
LatitudeConstants and LongitudeConstants name the bounds their types are defined by, and
GeoBoundsConstants.wholeWorld is the box built from all 4. GeohashConstants.first is the floor
of the sort order. There's no ceiling to name, since any geohash takes another z.
One shape, every type
GeoCoordinate.tryParse(input)hands back the value, ornullwhen the input isn't validGeoCoordinate.parse(input)hands back aParseOutcome: the value, or a typedGeoCoordinateFailureyou canswitchon, or read as a form-field message via.reasonOrNull. No door throws- value equality, a canonical form normalised on parse (
.iso6709,Geohash.value), andfromfor parts you already hold, which forGeohashreturns the value rather than an outcome
The minted README is the family guide: the package index,
handling failures, and the one caveat (never cast into a minted type).
APPENDIX.md carries the design rationale for these types: the alternatives that were weighed and dropped, and what each decision costs.
Libraries
- minted_geography
- Geographic coordinates as well-modelled value types.