INFRA: Set Up Project.

This commit is contained in:
2025-11-28 11:10:49 +05:00
commit c798279f7d
609 changed files with 77436 additions and 0 deletions

View 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();
}
}

View 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?;
}
}

View 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,
);
}
}

View 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);
}
}

View File

@@ -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,
});
}

View 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);
}

View File

@@ -0,0 +1,5 @@
extension NullableListExtensions<T> on Iterable<T?> {
Iterable<T> whereNotNull() {
return where((e) => e != null).map((e) => e as T);
}
}

View 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);
}
}