flutter_geo_hash 0.0.7 copy "flutter_geo_hash: ^0.0.7" to clipboard
flutter_geo_hash: ^0.0.7 copied to clipboard

Geohash encoding, decoding and neighbors, with Firestore geoquery bounds and distance helpers for Dart and Flutter. Compatible with Firebase geofire-common.

flutter_geo_hash #

pub package pub points CI License: MIT

Geohashes for Dart and Flutter. Encode and decode locations, find neighboring cells, and query the locations within a radius in Cloud Firestore or any other database that supports string range queries.

The encoding and query algorithms follow Firebase's geofire-common, the library used in Firebase's geoqueries guide. The geohashes are identical to the ones geofire-common computes, so a Flutter app and a web app can store and query the same documents.

Table of contents #

Features #

  • Encode a location into a geohash of 1 to 22 characters.
  • Query by radius: compute the geohash ranges that cover a circle, then discard the false positives by distance.
  • Decode a geohash into the center or the bounds of its cell.
  • Find neighbors: get the eight cells around a geohash.
  • Validate geohashes, locations and GeoFire keys.
  • No name clashes: the class names don't collide with the ones of cloud_firestore, popular map and location packages, or other geohash packages.
  • Pure Dart: runs in Flutter apps, Dart servers and command-line tools, on every platform including the web.

Installation #

In a Flutter project:

flutter pub add flutter_geo_hash

In a Dart project:

dart pub add flutter_geo_hash

Usage #

import 'package:flutter_geo_hash/flutter_geo_hash.dart';

void main() {
  final geoHash = GeoHashUtils();
  const london = GeoHashPoint(51.5074, -0.1278);
  const paris = GeoHashPoint(48.8566, 2.3522);

  // Encode a location. Longer geohashes are more precise.
  geoHash.geoHashForLocation(london); // 'gcpvj0duq5'
  geoHash.geoHashForLocation(london, precision: 5); // 'gcpvj'

  // Decode a geohash into the center or the bounds of its cell.
  geoHash.decodeGeoHash('gcpvj'); // GeoHashPoint(51.52587890625, -0.10986328125)
  geoHash.decodeGeoHashBounds('gcpvj').southWest; // GeoHashPoint(51.50390625, -0.1318359375)

  // Get the cells around a geohash.
  geoHash.geoHashNeighbors('gcpvj')[GeoHashDirection.north]; // 'gcpvm'

  // Measure distances.
  geoHash.distanceBetween(london, paris); // 343.556... (kilometers)
  geoHash.isWithinRadius(london, paris, 350000); // true (meters)

  // Validate input.
  geoHash.isValidGeoHash('gcpvj'); // true
  geoHash.isValidGeoHash('GCPVJ'); // false, geohashes are lowercase
}

A complete, runnable example is available in example/example.dart.

Querying Cloud Firestore by distance #

Firestore can't filter documents by distance, but it can query a range of strings. Storing a geohash with each document lets you find the documents around a location with a few range queries.

Store a geohash with each document #

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_geo_hash/flutter_geo_hash.dart';

final geoHash = GeoHashUtils();

Future<void> saveCity(String id, double lat, double lng) {
  return FirebaseFirestore.instance.collection('cities').doc(id).set({
    // Used to query by location.
    'geohash': geoHash.geoHashForLocation(GeoHashPoint(lat, lng)),
    // Used to discard false positives.
    'lat': lat,
    'lng': lng,
  }, SetOptions(merge: true));
}

Query the documents within a radius #

Future<List<DocumentSnapshot<Map<String, dynamic>>>> citiesWithin(
  GeoHashPoint center,
  double radiusInM,
) async {
  // Each range needs its own query; there are at most nine.
  final bounds = geoHash.geohashQueryBounds(center, radiusInM);
  final snapshots = await Future.wait([
    for (final range in bounds)
      FirebaseFirestore.instance
          .collection('cities')
          .orderBy('geohash')
          .startAt([range[0]])
          .endAt([range[1]])
          .get(),
  ]);

  // The ranges cover rectangles, so some matches are outside the circle.
  return [
    for (final snapshot in snapshots)
      for (final doc in snapshot.docs)
        if (geoHash.isWithinRadius(
          center,
          GeoHashPoint(
            (doc['lat'] as num).toDouble(),
            (doc['lng'] as num).toDouble(),
          ),
          radiusInM,
        ))
          doc,
  ];
}

geohashQueryBounds and isWithinRadius take the radius in meters, while distanceBetween returns kilometers.

Documents that store a Firestore GeoPoint #

To use a Firestore GeoPoint field, convert it to a GeoHashPoint:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_geo_hash/flutter_geo_hash.dart' hide GeoPoint;

GeoHashPoint toGeoHashPoint(GeoPoint location) =>
    GeoHashPoint(location.latitude, location.longitude);

Hiding GeoPoint from this package avoids a clash with the deprecated alias of the same name. It won't be needed once the alias is removed.

Choosing a precision #

geoHashForLocation returns 10-character geohashes by default. Store geohashes with that precision: a query only finds documents whose geohash is at least as long as its ranges, which are at most 10 characters long for any radius of 1 meter or more. Shorter geohashes are useful to group nearby locations, for example with geoHashNeighbors.

Length Approximate cell size at the equator (width × height)
1 5,009 km × 5,009 km
2 1,252 km × 626 km
3 157 km × 157 km
4 39.1 km × 19.6 km
5 4.89 km × 4.89 km
6 1.22 km × 611 m
7 153 m × 153 m
8 38.2 m × 19.1 m
9 4.78 m × 4.78 m
10 1.19 m × 597 mm
11 149 mm × 149 mm
12 37.3 mm × 18.7 mm

Cells get narrower towards the poles.

How geohashes work #

A geohash encodes a location as a short string. Each character splits the cell of the previous characters into 32 smaller cells, so the geohash of every location in a cell starts with the geohash of that cell, and locations that share a long prefix are close to each other.

The converse is not true: two locations on either side of a cell border can be very close to each other and still have very different geohashes. That's why geohashQueryBounds returns several ranges.

Nearby locations with different geohashes

API overview #

All the members below belong to GeoHashUtils, which has no state. Locations are GeoHashPoints, decoded cells are GeoHashBounds, and neighbors are keyed by GeoHashDirection.

Member Description
geoHashForLocation(location, {precision}) Returns the geohash of a location.
geohashQueryBounds(center, radius) Returns the geohash ranges covering a circle.
isWithinRadius(center, location, radiusInM) Whether a location is within a radius.
distanceBetween(location1, location2) Returns the distance between two locations, in kilometers.
decodeGeoHash(geoHash) Returns the center of a geohash cell.
decodeGeoHashBounds(geoHash) Returns the bounds of a geohash cell.
geoHashNeighbors(geoHash) Returns the cells around a geohash, by direction.
isValidGeoHash(geoHash) Whether a string is a valid geohash.
validateLocation(location), validateKey(key) Throw if a location or a GeoFire key is invalid.

Lower-level helpers, such as geohashQuery, boundingBoxCoordinates and wrapLongitude, are described in the API reference.

Error handling #

Invalid input Members Throws
Location geoHashForLocation, geohashQueryBounds, distanceBetween, isWithinRadius, validateLocation String
Precision outside 1 to 22 geoHashForLocation String
GeoFire key validateKey String
Geohash geohashQuery String
Negative, NaN or infinite radius geohashQueryBounds, isWithinRadius ArgumentError
Geohash decodeGeoHash, decodeGeoHashBounds, geoHashNeighbors FormatException

Errors that earlier versions threw as a String are still thrown as a String, so existing catch clauses keep working. In debug mode, the GeoHashPoint constructor also asserts that its coordinates are within range.

Migrating from earlier versions #

Version 0.0.7 is a drop-in upgrade: code written for earlier versions keeps compiling, geohashes are unchanged, and query ranges only change where earlier versions missed results. See the changelog for details.

Two classes have new names, which don't clash with other packages:

Before After
MyGeoHash GeoHashUtils
GeoPoint GeoHashPoint

The old names still work, but they are deprecated and will be removed in a future breaking release. To update your code automatically, run:

dart fix --apply

Limitations #

  • False positives. The ranges cover rectangles, so queries also return some documents outside the circle. Discard them with isWithinRadius. With Firestore, they still count as document reads.
  • Poles and large radii. Near the poles and for radii of thousands of kilometers, the ranges become coarse, down to a whole hemisphere, so queries read more documents.
  • Approximate distances. distanceBetween uses the haversine formula on a spherical earth, which can differ from the actual distance by about 0.5%.

Contributing #

Issues and pull requests are welcome on GitHub. Before opening a pull request, please run:

dart pub get
dart format .
dart analyze
dart test

License #

MIT

Author #

Created and maintained by Miracle Okolo. Connect on LinkedIn.

10
likes
160
points
3.56k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Geohash encoding, decoding and neighbors, with Firestore geoquery bounds and distance helpers for Dart and Flutter. Compatible with Firebase geofire-common.

Repository (GitHub)
View/report issues

Topics

#geohash #geolocation #firestore #firebase #maps

License

MIT (license)

More

Packages that depend on flutter_geo_hash