INFRA: Set Up Project.
This commit is contained in:
189
lib/widget/geoflutterfire/src/collection/base.dart
Normal file
189
lib/widget/geoflutterfire/src/collection/base.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../models/distance_doc_snapshot.dart';
|
||||
import '../models/point.dart';
|
||||
import '../utils/arrays.dart';
|
||||
import '../utils/math.dart';
|
||||
|
||||
class BaseGeoFireCollectionRef<T> {
|
||||
final Query<T> _collectionReference;
|
||||
late final Stream<QuerySnapshot<T>>? _stream;
|
||||
|
||||
BaseGeoFireCollectionRef(this._collectionReference) {
|
||||
_stream = _createStream(_collectionReference).shareReplay(maxSize: 1);
|
||||
}
|
||||
|
||||
/// return QuerySnapshot stream
|
||||
Stream<QuerySnapshot<T>>? snapshot() {
|
||||
return _stream;
|
||||
}
|
||||
|
||||
/// return the Document mapped to the [id]
|
||||
Stream<List<DocumentSnapshot<T>>> data(String id) {
|
||||
return _stream!.map((querySnapshot) {
|
||||
querySnapshot.docs.where((documentSnapshot) {
|
||||
return documentSnapshot.id == id;
|
||||
});
|
||||
return querySnapshot.docs;
|
||||
});
|
||||
}
|
||||
|
||||
/// add a document to collection with [data]
|
||||
Future<DocumentReference<T>> add(
|
||||
T data,
|
||||
) {
|
||||
try {
|
||||
final colRef = _collectionReference as CollectionReference<T>;
|
||||
return colRef.add(data);
|
||||
} catch (e) {
|
||||
throw Exception('cannot call add on Query, use collection reference instead');
|
||||
}
|
||||
}
|
||||
|
||||
/// delete document with [id] from the collection
|
||||
Future<void> delete(id) {
|
||||
try {
|
||||
CollectionReference colRef = _collectionReference as CollectionReference;
|
||||
return colRef.doc(id).delete();
|
||||
} catch (e) {
|
||||
throw Exception('cannot call delete on Query, use collection reference instead');
|
||||
}
|
||||
}
|
||||
|
||||
/// create or update a document with [id], [merge] defines whether the document should overwrite
|
||||
Future<void> setDoc(String id, Object? data, {bool merge = false}) {
|
||||
try {
|
||||
CollectionReference colRef = _collectionReference as CollectionReference;
|
||||
return colRef.doc(id).set(data, SetOptions(merge: merge));
|
||||
} catch (e) {
|
||||
throw Exception('cannot call set on Query, use collection reference instead');
|
||||
}
|
||||
}
|
||||
|
||||
/// set a geo point with [latitude] and [longitude] using [field] as the object key to the document with [id]
|
||||
Future<void> setPoint(
|
||||
String id,
|
||||
String field,
|
||||
double latitude,
|
||||
double longitude,
|
||||
) {
|
||||
try {
|
||||
CollectionReference colRef = _collectionReference as CollectionReference;
|
||||
var point = GeoFirePoint(latitude, longitude).data;
|
||||
return colRef.doc(id).set({field: point}, SetOptions(merge: true));
|
||||
} catch (e) {
|
||||
throw Exception('cannot call set on Query, use collection reference instead');
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
Stream<List<DocumentSnapshot<T>>> protectedWithin({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
required GeoPoint? Function(T t) geopointFrom,
|
||||
required bool? strictMode,
|
||||
}) =>
|
||||
protectedWithinWithDistance(
|
||||
center: center,
|
||||
radius: radius,
|
||||
field: field,
|
||||
geopointFrom: geopointFrom,
|
||||
strictMode: strictMode,
|
||||
).map((snapshots) => snapshots.map((snapshot) => snapshot.documentSnapshot).toList());
|
||||
|
||||
/// query firestore documents based on geographic [radius] from geoFirePoint [center]
|
||||
/// [field] specifies the name of the key in the document
|
||||
@protected
|
||||
Stream<List<DistanceDocSnapshot<T>>> protectedWithinWithDistance({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
required GeoPoint? Function(T t) geopointFrom,
|
||||
required bool? strictMode,
|
||||
}) {
|
||||
final nonNullStrictMode = strictMode ?? false;
|
||||
|
||||
final precision = MathUtils.setPrecision(radius);
|
||||
final centerHash = center.hash.substring(0, precision);
|
||||
final area = GeoFirePoint.neighborsOf(hash: centerHash)..add(centerHash);
|
||||
|
||||
final queries = area.map((hash) {
|
||||
final tempQuery = _queryPoint(hash, field);
|
||||
return _createStream(tempQuery).map((querySnapshot) {
|
||||
return querySnapshot.docs;
|
||||
});
|
||||
});
|
||||
|
||||
final mergedObservable = mergeObservable(queries);
|
||||
|
||||
final filtered = mergedObservable.map((list) {
|
||||
final mappedList = list.map((documentSnapshot) {
|
||||
final snapData = documentSnapshot.exists ? documentSnapshot.data() : null;
|
||||
|
||||
assert(snapData != null, 'Data in one of the docs is empty');
|
||||
if (snapData == null) return null;
|
||||
// We will handle it to fail gracefully
|
||||
|
||||
final geoPoint = geopointFrom(snapData);
|
||||
assert(geoPoint != null, 'Couldnt find geopoint from stored data');
|
||||
if (geoPoint == null) return null;
|
||||
// We will handle it to fail gracefully
|
||||
|
||||
final kmDistance = center.kmDistance(
|
||||
lat: geoPoint.latitude,
|
||||
lng: geoPoint.longitude,
|
||||
);
|
||||
return DistanceDocSnapshot(
|
||||
documentSnapshot: documentSnapshot,
|
||||
kmDistance: kmDistance,
|
||||
);
|
||||
});
|
||||
|
||||
final nullableFilteredList = nonNullStrictMode
|
||||
? mappedList
|
||||
.where((doc) => doc != null && doc.kmDistance <= radius * 1.02 // buffer for edge distances;
|
||||
)
|
||||
.toList()
|
||||
: mappedList.toList();
|
||||
final filteredList = nullableFilteredList.whereNotNull().toList();
|
||||
|
||||
filteredList.sort(
|
||||
(a, b) => (a.kmDistance * 1000).toInt() - (b.kmDistance * 1000).toInt(),
|
||||
);
|
||||
return filteredList;
|
||||
});
|
||||
return filtered.asBroadcastStream();
|
||||
}
|
||||
|
||||
Stream<List<QueryDocumentSnapshot<T>>> mergeObservable(
|
||||
Iterable<Stream<List<QueryDocumentSnapshot<T>>>> queries,
|
||||
) {
|
||||
final mergedObservable = Rx.combineLatest<List<QueryDocumentSnapshot<T>>, List<QueryDocumentSnapshot<T>>>(queries, (originalList) {
|
||||
final reducedList = <QueryDocumentSnapshot<T>>[];
|
||||
for (final t in originalList) {
|
||||
reducedList.addAll(t);
|
||||
}
|
||||
return reducedList;
|
||||
});
|
||||
return mergedObservable;
|
||||
}
|
||||
|
||||
/// INTERNAL FUNCTIONS
|
||||
|
||||
/// construct a query for the [geoHash] and [field]
|
||||
Query<T> _queryPoint(String geoHash, String field) {
|
||||
final end = '$geoHash~';
|
||||
final temp = _collectionReference;
|
||||
return temp.orderBy('$field.geohash').startAt([geoHash]).endAt([end]);
|
||||
}
|
||||
|
||||
/// create an observable for [ref], [ref] can be [Query] or [CollectionReference]
|
||||
Stream<QuerySnapshot<T>> _createStream(Query<T> ref) {
|
||||
return ref.snapshots();
|
||||
}
|
||||
}
|
||||
64
lib/widget/geoflutterfire/src/collection/default.dart
Normal file
64
lib/widget/geoflutterfire/src/collection/default.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:customer/widget/geoflutterfire/src/models/distance_doc_snapshot.dart';
|
||||
import 'package:customer/widget/geoflutterfire/src/models/point.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class GeoFireCollectionRef extends BaseGeoFireCollectionRef<Map<String, dynamic>> {
|
||||
GeoFireCollectionRef(super.collectionReference);
|
||||
|
||||
Stream<List<DocumentSnapshot<Map<String, dynamic>>>> within({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
bool? strictMode,
|
||||
}) {
|
||||
return protectedWithin(
|
||||
center: center,
|
||||
radius: radius,
|
||||
field: field,
|
||||
geopointFrom: (snapData) => geopointFromMap(
|
||||
field: field,
|
||||
snapData: snapData,
|
||||
),
|
||||
strictMode: strictMode,
|
||||
);
|
||||
}
|
||||
|
||||
Stream<List<DistanceDocSnapshot<Map<String, dynamic>>>> withinWithDistance({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
bool? strictMode,
|
||||
}) {
|
||||
return protectedWithinWithDistance(
|
||||
center: center,
|
||||
radius: radius,
|
||||
field: field,
|
||||
geopointFrom: (snapData) => geopointFromMap(
|
||||
field: field,
|
||||
snapData: snapData,
|
||||
),
|
||||
strictMode: strictMode,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static GeoPoint? geopointFromMap({
|
||||
required String field,
|
||||
required Map<String, dynamic> snapData,
|
||||
}) {
|
||||
// split and fetch geoPoint from the nested Map
|
||||
final fieldList = field.split('.');
|
||||
Map<dynamic, dynamic>? geoPointField = snapData[fieldList[0]];
|
||||
if (fieldList.length > 1) {
|
||||
for (int i = 1; i < fieldList.length; i++) {
|
||||
geoPointField = geoPointField?[fieldList[i]];
|
||||
}
|
||||
}
|
||||
return geoPointField?['geopoint'] as GeoPoint?;
|
||||
}
|
||||
}
|
||||
43
lib/widget/geoflutterfire/src/collection/with_converter.dart
Normal file
43
lib/widget/geoflutterfire/src/collection/with_converter.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:customer/widget/geoflutterfire/src/models/distance_doc_snapshot.dart';
|
||||
import 'package:customer/widget/geoflutterfire/src/models/point.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
class GeoFireCollectionWithConverterRef<T> extends BaseGeoFireCollectionRef<T> {
|
||||
GeoFireCollectionWithConverterRef(super.collectionReference);
|
||||
|
||||
Stream<List<DocumentSnapshot<T>>> within({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
required GeoPoint Function(T) geopointFrom,
|
||||
bool? strictMode,
|
||||
}) {
|
||||
return protectedWithin(
|
||||
center: center,
|
||||
radius: radius,
|
||||
field: field,
|
||||
geopointFrom: geopointFrom,
|
||||
strictMode: strictMode,
|
||||
);
|
||||
}
|
||||
|
||||
Stream<List<DistanceDocSnapshot<T>>> withinWithDistance({
|
||||
required GeoFirePoint center,
|
||||
required double radius,
|
||||
required String field,
|
||||
required GeoPoint Function(T) geopointFrom,
|
||||
bool? strictMode,
|
||||
}) {
|
||||
return protectedWithinWithDistance(
|
||||
center: center,
|
||||
radius: radius,
|
||||
field: field,
|
||||
geopointFrom: geopointFrom,
|
||||
strictMode: strictMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/widget/geoflutterfire/src/geoflutterfire.dart
Normal file
31
lib/widget/geoflutterfire/src/geoflutterfire.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:customer/widget/geoflutterfire/src/collection/with_converter.dart';
|
||||
|
||||
import 'collection/default.dart';
|
||||
import 'models/point.dart';
|
||||
|
||||
class Geoflutterfire {
|
||||
Geoflutterfire();
|
||||
|
||||
GeoFireCollectionRef collection({
|
||||
required Query<Map<String, dynamic>> collectionRef,
|
||||
}) {
|
||||
return GeoFireCollectionRef(collectionRef);
|
||||
}
|
||||
|
||||
GeoFireCollectionWithConverterRef<T> collectionWithConverter<T>({
|
||||
required Query<T> collectionRef,
|
||||
}) {
|
||||
return GeoFireCollectionWithConverterRef<T>(collectionRef);
|
||||
}
|
||||
|
||||
GeoFireCollectionRef customCollection({
|
||||
required Query<Map<String, dynamic>> collectionRef,
|
||||
}) {
|
||||
return GeoFireCollectionRef(collectionRef);
|
||||
}
|
||||
|
||||
GeoFirePoint point({required double latitude, required double longitude}) {
|
||||
return GeoFirePoint(latitude, longitude);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class DistanceDocSnapshot<T> {
|
||||
final DocumentSnapshot<T> documentSnapshot;
|
||||
final double kmDistance;
|
||||
|
||||
DistanceDocSnapshot({
|
||||
required this.documentSnapshot,
|
||||
required this.kmDistance,
|
||||
});
|
||||
}
|
||||
60
lib/widget/geoflutterfire/src/models/point.dart
Normal file
60
lib/widget/geoflutterfire/src/models/point.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
import '../utils/math.dart';
|
||||
|
||||
class GeoFirePoint {
|
||||
static final MathUtils _util = MathUtils();
|
||||
double latitude, longitude;
|
||||
|
||||
GeoFirePoint(this.latitude, this.longitude);
|
||||
|
||||
/// return geographical distance between two Co-ordinates
|
||||
static double kmDistanceBetween({required Coordinates to, required Coordinates from}) {
|
||||
return MathUtils.kmDistance(to, from);
|
||||
}
|
||||
|
||||
/// return neighboring geo-hashes of [hash]
|
||||
static List<String> neighborsOf({required String hash}) {
|
||||
return _util.neighbors(hash);
|
||||
}
|
||||
|
||||
/// return hash of [GeoFirePoint]
|
||||
String get hash {
|
||||
return _util.encode(latitude, longitude, 9);
|
||||
}
|
||||
|
||||
/// return all neighbors of [GeoFirePoint]
|
||||
List<String> get neighbors {
|
||||
return _util.neighbors(hash);
|
||||
}
|
||||
|
||||
/// return [GeoPoint] of [GeoFirePoint]
|
||||
GeoPoint get geoPoint {
|
||||
return GeoPoint(latitude, longitude);
|
||||
}
|
||||
|
||||
Coordinates get coords {
|
||||
return Coordinates(latitude, longitude);
|
||||
}
|
||||
|
||||
/// return distance between [GeoFirePoint] and ([lat], [lng])
|
||||
double kmDistance({required double lat, required double lng}) {
|
||||
return kmDistanceBetween(from: coords, to: Coordinates(lat, lng));
|
||||
}
|
||||
|
||||
Map<String, Object> get data {
|
||||
return {'geopoint': geoPoint, 'geohash': hash};
|
||||
}
|
||||
|
||||
/// haversine distance between [GeoFirePoint] and ([lat], [lng])
|
||||
double haversineDistance({required double lat, required double lng}) {
|
||||
return GeoFirePoint.kmDistanceBetween(from: coords, to: Coordinates(lat, lng));
|
||||
}
|
||||
}
|
||||
|
||||
class Coordinates {
|
||||
double latitude;
|
||||
double longitude;
|
||||
|
||||
Coordinates(this.latitude, this.longitude);
|
||||
}
|
||||
5
lib/widget/geoflutterfire/src/utils/arrays.dart
Normal file
5
lib/widget/geoflutterfire/src/utils/arrays.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
extension NullableListExtensions<T> on Iterable<T?> {
|
||||
Iterable<T> whereNotNull() {
|
||||
return where((e) => e != null).map((e) => e as T);
|
||||
}
|
||||
}
|
||||
271
lib/widget/geoflutterfire/src/utils/math.dart
Normal file
271
lib/widget/geoflutterfire/src/utils/math.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'dart:math';
|
||||
|
||||
import '../models/point.dart';
|
||||
|
||||
class MathUtils {
|
||||
static const base32Codes = '0123456789bcdefghjkmnpqrstuvwxyz';
|
||||
Map<String, int> base32CodesDic = {};
|
||||
|
||||
MathUtils() {
|
||||
for (var i = 0; i < base32Codes.length; i++) {
|
||||
base32CodesDic.putIfAbsent(base32Codes[i], () => i);
|
||||
}
|
||||
}
|
||||
|
||||
var encodeAuto = 'auto';
|
||||
|
||||
///
|
||||
/// Significant Figure Hash Length
|
||||
///
|
||||
/// This is a quick and dirty lookup to figure out how long our hash
|
||||
/// should be in order to guarantee a certain amount of trailing
|
||||
/// significant figures. This was calculated by determining the error:
|
||||
/// 45/2^(n-1) where n is the number of bits for a latitude or
|
||||
/// longitude. Key is # of desired sig figs, value is minimum length of
|
||||
/// the geohash.
|
||||
/// @type Array
|
||||
// Desired sig figs: 0 1 2 3 4 5 6 7 8 9 10
|
||||
var sigfigHashLength = [0, 5, 7, 8, 11, 12, 13, 15, 16, 17, 18];
|
||||
|
||||
///
|
||||
/// Encode
|
||||
/// Create a geohash from latitude and longitude
|
||||
/// that is 'number of chars' long
|
||||
String encode(var latitude, var longitude, var numberOfChars) {
|
||||
if (numberOfChars == encodeAuto) {
|
||||
if (latitude.runtimeType == double || longitude.runtimeType == double) {
|
||||
throw Exception('string notation required for auto precision.');
|
||||
}
|
||||
int decSigFigsLat = latitude.split('.')[1].length;
|
||||
int decSigFigsLon = longitude.split('.')[1].length;
|
||||
int numberOfSigFigs = max(decSigFigsLat, decSigFigsLon);
|
||||
numberOfChars = sigfigHashLength[numberOfSigFigs];
|
||||
} else {
|
||||
numberOfChars ??= 9;
|
||||
}
|
||||
|
||||
var chars = [], bits = 0, bitsTotal = 0, hashValue = 0;
|
||||
double maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid;
|
||||
|
||||
while (chars.length < numberOfChars) {
|
||||
if (bitsTotal % 2 == 0) {
|
||||
mid = (maxLon + minLon) / 2;
|
||||
if (longitude > mid) {
|
||||
hashValue = (hashValue << 1) + 1;
|
||||
minLon = mid;
|
||||
} else {
|
||||
hashValue = (hashValue << 1) + 0;
|
||||
maxLon = mid;
|
||||
}
|
||||
} else {
|
||||
mid = (maxLat + minLat) / 2;
|
||||
if (latitude > mid) {
|
||||
hashValue = (hashValue << 1) + 1;
|
||||
minLat = mid;
|
||||
} else {
|
||||
hashValue = (hashValue << 1) + 0;
|
||||
maxLat = mid;
|
||||
}
|
||||
}
|
||||
|
||||
bits++;
|
||||
bitsTotal++;
|
||||
if (bits == 5) {
|
||||
var code = base32Codes[hashValue];
|
||||
chars.add(code);
|
||||
bits = 0;
|
||||
hashValue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
///
|
||||
/// Decode Bounding box
|
||||
///
|
||||
/// Decode a hashString into a bound box that matches it.
|
||||
/// Data returned in a List [minLat, minLon, maxLat, maxLon]
|
||||
List<double> decodeBbox(String hashString) {
|
||||
var isLon = true;
|
||||
double maxLat = 90, minLat = -90, maxLon = 180, minLon = -180, mid;
|
||||
|
||||
int? hashValue = 0;
|
||||
for (var i = 0, l = hashString.length; i < l; i++) {
|
||||
var code = hashString[i].toLowerCase();
|
||||
hashValue = base32CodesDic[code];
|
||||
|
||||
for (var bits = 4; bits >= 0; bits--) {
|
||||
var bit = (hashValue! >> bits) & 1;
|
||||
if (isLon) {
|
||||
mid = (maxLon + minLon) / 2;
|
||||
if (bit == 1) {
|
||||
minLon = mid;
|
||||
} else {
|
||||
maxLon = mid;
|
||||
}
|
||||
} else {
|
||||
mid = (maxLat + minLat) / 2;
|
||||
if (bit == 1) {
|
||||
minLat = mid;
|
||||
} else {
|
||||
maxLat = mid;
|
||||
}
|
||||
}
|
||||
isLon = !isLon;
|
||||
}
|
||||
}
|
||||
return [minLat, minLon, maxLat, maxLon];
|
||||
}
|
||||
|
||||
///
|
||||
/// Decode a [hashString] into a pair of latitude and longitude.
|
||||
/// A map is returned with keys 'latitude', 'longitude','latitudeError','longitudeError'
|
||||
Map<String, double> decode(String hashString) {
|
||||
List<double> bbox = decodeBbox(hashString);
|
||||
double lat = (bbox[0] + bbox[2]) / 2;
|
||||
double lon = (bbox[1] + bbox[3]) / 2;
|
||||
double latErr = bbox[2] - lat;
|
||||
double lonErr = bbox[3] - lon;
|
||||
return {
|
||||
'latitude': lat,
|
||||
'longitude': lon,
|
||||
'latitudeError': latErr,
|
||||
'longitudeError': lonErr,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Neighbor
|
||||
///
|
||||
/// Find neighbor of a geohash string in certain direction.
|
||||
/// Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest.
|
||||
///
|
||||
/// direction [lat, lon], i.e.
|
||||
/// [1,0] - north
|
||||
/// [1,1] - northeast
|
||||
String neighbor(String hashString, var direction) {
|
||||
var lonLat = decode(hashString);
|
||||
var neighborLat = lonLat['latitude']! + direction[0] * lonLat['latitudeError'] * 2;
|
||||
var neighborLon = lonLat['longitude']! + direction[1] * lonLat['longitudeError'] * 2;
|
||||
return encode(neighborLat, neighborLon, hashString.length);
|
||||
}
|
||||
|
||||
///
|
||||
/// Neighbors
|
||||
/// Returns all neighbors' hashstrings clockwise from north around to northwest
|
||||
/// 7 0 1
|
||||
/// 6 X 2
|
||||
/// 5 4 3
|
||||
List<String> neighbors(String hashString) {
|
||||
int hashStringLength = hashString.length;
|
||||
var lonlat = decode(hashString);
|
||||
double? lat = lonlat['latitude'];
|
||||
double? lon = lonlat['longitude'];
|
||||
double latErr = lonlat['latitudeError']! * 2;
|
||||
double lonErr = lonlat['longitudeError']! * 2;
|
||||
|
||||
num neighborLat, neighborLon;
|
||||
|
||||
String encodeNeighbor(num neighborLatDir, num neighborLonDir) {
|
||||
neighborLat = lat! + neighborLatDir * latErr;
|
||||
neighborLon = lon! + neighborLonDir * lonErr;
|
||||
return encode(neighborLat, neighborLon, hashStringLength);
|
||||
}
|
||||
|
||||
var neighborHashList = [
|
||||
encodeNeighbor(1, 0),
|
||||
encodeNeighbor(1, 1),
|
||||
encodeNeighbor(0, 1),
|
||||
encodeNeighbor(-1, 1),
|
||||
encodeNeighbor(-1, 0),
|
||||
encodeNeighbor(-1, -1),
|
||||
encodeNeighbor(0, -1),
|
||||
encodeNeighbor(1, -1)
|
||||
];
|
||||
|
||||
return neighborHashList;
|
||||
}
|
||||
|
||||
static int setPrecision(double km) {
|
||||
/*
|
||||
* 1 ≤ 5,000km × 5,000km
|
||||
* 2 ≤ 1,250km × 625km
|
||||
* 3 ≤ 156km × 156km
|
||||
* 4 ≤ 39.1km × 19.5km
|
||||
* 5 ≤ 4.89km × 4.89km
|
||||
* 6 ≤ 1.22km × 0.61km
|
||||
* 7 ≤ 153m × 153m
|
||||
* 8 ≤ 38.2m × 19.1m
|
||||
* 9 ≤ 4.77m × 4.77m
|
||||
*
|
||||
*/
|
||||
|
||||
if (km <= 0.00477) {
|
||||
return 9;
|
||||
} else if (km <= 0.0382) {
|
||||
return 8;
|
||||
} else if (km <= 0.153) {
|
||||
return 7;
|
||||
} else if (km <= 1.22) {
|
||||
return 6;
|
||||
} else if (km <= 4.89) {
|
||||
return 5;
|
||||
} else if (km <= 39.1) {
|
||||
return 4;
|
||||
} else if (km <= 156) {
|
||||
return 3;
|
||||
} else if (km <= 1250) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static const double maxSupportedRadius = 8587;
|
||||
|
||||
// Length of a degree latitude at the equator
|
||||
static const double metersPerDegreeLatitude = 110574;
|
||||
|
||||
// The equatorial circumference of the earth in meters
|
||||
static const double earthMeridionalCircumference = 40007860;
|
||||
|
||||
// The equatorial radius of the earth in meters
|
||||
static const double earthEqRadius = 6378137;
|
||||
|
||||
// The meridional radius of the earth in meters
|
||||
static const double earthPolarRadius = 6357852.3;
|
||||
|
||||
/* The following value assumes a polar radius of
|
||||
* r_p = 6356752.3
|
||||
* and an equatorial radius of
|
||||
* r_e = 6378137
|
||||
* The value is calculated as e2 == (r_e^2 - r_p^2)/(r_e^2)
|
||||
* Use exact value to avoid rounding errors
|
||||
*/
|
||||
static const double earthE2 = 0.00669447819799;
|
||||
|
||||
// Cutoff for floating point calculations
|
||||
static const double epsilon = 1e-12;
|
||||
|
||||
/// distance in km
|
||||
static double kmDistance(Coordinates location1, Coordinates location2) {
|
||||
return kmCalcDistance(location1.latitude, location1.longitude, location2.latitude, location2.longitude);
|
||||
}
|
||||
|
||||
/// distance in km
|
||||
static double kmCalcDistance(double lat1, double long1, double lat2, double long2) {
|
||||
// Earth's mean radius in meters
|
||||
const radius = (earthEqRadius + earthPolarRadius) / 2;
|
||||
double latDelta = _toRadians(lat1 - lat2);
|
||||
double lonDelta = _toRadians(long1 - long2);
|
||||
|
||||
double a = (sin(latDelta / 2) * sin(latDelta / 2)) + (cos(_toRadians(lat1)) * cos(_toRadians(lat2)) * sin(lonDelta / 2) * sin(lonDelta / 2));
|
||||
double distance = radius * 2 * atan2(sqrt(a), sqrt(1 - a)) / 1000;
|
||||
return double.parse(distance.toStringAsFixed(3));
|
||||
}
|
||||
|
||||
static double _toRadians(double num) {
|
||||
return num * (pi / 180.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user