forCircle method
- double lat,
- double lon,
- double radius,
- int precision, {
- int limit = 10000,
- double latLimit = 86,
- bool asBox = false,
- bool precise = false,
Returns a list of geohashes for a circle with a given centre location and a radius in meters. Assumes the latitude and longitude to be in EPSG:4326 projection. Will trim the circle if it goes outside the EPSG:3857 projection boundaries.
Note that this by default uses an equirectangular approximation for
distances. It works well on distances under a couple hundred kilometers.
If you need higher precision and bigger distances, set precise
to true. It will slow down processing approximately by 2.75 times
(still a fraction of a millisecond).
Keep in mind that a notion of a circle on a sphere is a bit complex, so the result can still be incorrect.
Adjust the limit to throw TooManyGeohashesException when there
are too many cells covered. To guard against geohashes expanding
around poles, set latLimit to cut off processing behind those latitudes.
Implementation
List<T> forCircle(double lat, double lon, double radius, int precision,
{int limit = 10000,
double latLimit = 86,
bool asBox = false,
bool precise = false}) {
if (precision > maxPrecision || precision < 1) {
throw ArgumentError('Precision should be between 1 and $maxPrecision');
}
if (radius < 0) {
throw ArgumentError('Radius should be a positive number');
}
if (latLimit < 80 || latLimit > 90) {
throw ArgumentError('Latitude limit should be between 80° and 90°.');
}
if (lat < -90 || lat > 90) {
throw ArgumentError('Latitude is out of bounds: $lat');
}
while (lon < -180) lon += 360;
while (lon > 180) lon -= 360;
final rRadius = radius / _kEarthRadius;
final rRadiusSq = rRadius * rRadius;
final rad = pi / 180;
final rLat = lat * rad;
final rLon = lon * rad;
final rLatLimit = latLimit * rad;
final cosLat = cos(rLat);
if (asBox) {
// Calculate the boundaries and call another function.
double radLon = rRadius / cosLat / rad;
double radLat = rRadius / rad;
return forBounds(max(-latLimit, lat - radLat), lon - radLon,
min(latLimit, lat + radLat), lon + radLon, precision);
}
final centre = encode(lat, lon, precision);
final b = bounds(centre); // to not re-encode every geohash
final dLat = (b.maxLat - b.minLat) * rad;
final dLon = (b.maxLon - b.minLon) * rad;
final rLeftLon = b.minLon * rad;
final rRightLon = b.maxLon * rad;
final result = <T>[];
// We don't start this entire thing when we're already over the limit.
if (lat < -latLimit || lat > latLimit) return [centre];
// Calculates distance **in radians** from the circle centre to the given
// location (also in radians). Returns true if the location is strictly
// inside the radius.
bool inside(double toLat, double toLon) {
final dy = toLat - rLat;
if (precise) {
return acos(sin(rLat) * sin(toLat) +
cos(rLat) * cos(toLat) * cos(rLon - toLon)) <
rRadius;
} else {
final dx = (toLon - rLon) * cosLat;
return dx * dx + dy * dy < rRadiusSq;
}
}
// Starting with the centre geohash (not added to the result), step left
// or right while the distance to the far corner of the geohash bounds
// (baseLat, sideLon) is inside the radius. All angles are in radians.
void traverseRow(T geohash, double baseLat) {
// Add the central geohash, since we're here because it's inside the radius.
result.add(geohash);
// First go left.
double sideLon = rLeftLon;
T current = geohash;
while (inside(baseLat, sideLon) && sideLon > rLon - pi) {
sideLon -= dLon;
current = adjacent(current, Direction.west);
result.add(current);
}
// And then go right.
sideLon = rRightLon;
current = geohash;
while (inside(baseLat, sideLon) && sideLon < rLon + pi) {
sideLon += dLon;
current = adjacent(current, Direction.east);
result.add(current);
}
if (result.length > limit) {
throw TooManyGeohashesException(result.length);
}
}
// Left and right.
traverseRow(centre, rLat);
// Go up while we can and also venture left and right.
double baseLat = b.maxLat * rad;
T current = centre;
while (inside(baseLat, rLon) && baseLat < rLatLimit) {
current = adjacent(current, Direction.north);
traverseRow(current, baseLat);
baseLat += dLat;
}
// And now go down doing the same thing.
baseLat = b.minLat * rad;
current = centre;
while (inside(baseLat, rLon) && baseLat > -rLatLimit) {
current = adjacent(current, Direction.south);
traverseRow(current, baseLat);
baseLat -= dLat;
}
return result;
}