Last active
January 1, 2021 21:14
-
-
Save lukepighetti/442fca7115c752b9a93b025fc04b4c18 to your computer and use it in GitHub Desktop.
https://github.com/synw/geojson/issues/33#issuecomment-753375534 ✓ Geofencing Search Extensions Loads GeoJSON file, gets bounding boxes, performs search
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/foundation.dart'; | |
import 'package:geojson/geojson.dart'; | |
extension GeoJsonSearchX on GeoJson { | |
/// Given a list of polygons, find which one contains a given point. | |
/// | |
/// If the point isn't within any of these polygons, return `null`. | |
Future<List<GeoJsonFeature<GeoJsonPolygon>>> geofenceSearch( | |
List<GeoJsonFeature<GeoJsonPolygon>> geofences, | |
GeoJsonPoint query, | |
) async { | |
final boundingBoxes = getBoundingBoxes(geofences); | |
final filteredGeofences = [ | |
for (var box in boundingBoxes) | |
if (box.contains(query.geoPoint.latitude, query.geoPoint.longitude)) | |
box.feature | |
]; | |
return _geofencesContainingPointNaive(filteredGeofences, query); | |
} | |
/// Return all geofences that contain the point provided. | |
/// | |
/// Naive implementation. The geofences should be filtered first using a method such | |
/// as searching bounding boxes first. | |
Future<List<GeoJsonFeature<GeoJsonPolygon>>> _geofencesContainingPointNaive( | |
List<GeoJsonFeature<GeoJsonPolygon>> geofences, | |
GeoJsonPoint query, | |
) async { | |
final futures = [ | |
for (var geofence in geofences) | |
geofencePolygon( | |
polygon: geofence.geometry, | |
points: [query], | |
).then((results) { | |
/// Nothing found | |
if (results.isEmpty) return null; | |
/// Found a result | |
if (results.first.name == query.name) return geofence; | |
}) | |
]; | |
final unfilteredResults = await Future.wait(futures); | |
return unfilteredResults.where((e) => e != null).toList(); | |
} | |
/// Given a set of geofence polygons, find all of their bounding boxes, and the index at which they were found. | |
List<GeoBoundingBox> getBoundingBoxes( | |
List<GeoJsonFeature<GeoJsonPolygon>> geofences) { | |
final boundingBoxes = <GeoBoundingBox>[]; | |
for (var i = 0; i <= geofences.length - 1; i++) { | |
final geofence = geofences[i]; | |
double maxLat; | |
double minLat; | |
double maxLong; | |
double minLong; | |
for (var geoSerie in geofence.geometry.geoSeries) { | |
for (var geoPoint in geoSerie.geoPoints) { | |
final lat = geoPoint.latitude; | |
final long = geoPoint.longitude; | |
/// Make sure they get seeded if they are null | |
maxLat ??= lat; | |
minLat ??= lat; | |
maxLong ??= long; | |
minLong ??= long; | |
/// Update values | |
if (maxLat < lat) maxLat = lat; | |
if (minLat > lat) minLat = lat; | |
if (maxLong < long) maxLong = long; | |
if (minLong > long) minLong = long; | |
} | |
} | |
boundingBoxes.add(GeoBoundingBox( | |
feature: geofence, | |
minLat: minLat, | |
maxLong: maxLong, | |
maxLat: maxLat, | |
minLong: minLong, | |
)); | |
} | |
return boundingBoxes; | |
} | |
} | |
class GeoBoundingBox { | |
/// A geographical rectangle. Typically used as a bounding box for a polygon | |
/// for fast search of point-in-multiple-polygon. | |
GeoBoundingBox({ | |
@required this.feature, | |
@required this.maxLat, | |
@required this.maxLong, | |
@required this.minLat, | |
@required this.minLong, | |
}); | |
/// The polygon bounded by this bounding box | |
final GeoJsonFeature<GeoJsonPolygon> feature; | |
final double maxLat; | |
final double maxLong; | |
final double minLat; | |
final double minLong; | |
double get left => minLat; | |
double get top => maxLong; | |
double get right => maxLat; | |
double get bottom => minLong; | |
bool contains(double lat, double long) { | |
final containsLat = maxLat >= lat && minLat <= lat; | |
final containsLong = maxLong >= long && minLong <= long; | |
return containsLat && containsLong; | |
} | |
@override | |
String toString() => 'GeoRect($minLat,$minLong,$maxLat,$maxLong)'; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter_test/flutter_test.dart'; | |
import 'package:geojson/geojson.dart'; | |
import 'package:mh/features/geofencing/geofencing_extensions.dart'; | |
void main() { | |
TestWidgetsFlutterBinding.ensureInitialized(); | |
group('Geofencing Search Extensions', () { | |
test('Loads GeoJSON file, gets bounding boxes, performs search', () async { | |
final geo = GeoJson(); | |
/// Test loading the GeoJSON file | |
final features = await geo.featuresFromGeoJsonAsset( | |
'assets/geojson/geofencing-search-test.json'); | |
expect(features.collection.length, equals(7)); | |
expect(features.polygons.length, equals(2)); | |
/// Test bounding boxes | |
final geofences = features.polygons; | |
final boundingBoxes = geo.getBoundingBoxes(geofences); | |
/// polygon-a | |
/// | |
/// latitude | |
/// min: 44.7120980759138 | |
/// max: 44.848567071337264 | |
/// | |
/// longitude | |
/// min: -68.92959594726562 | |
/// max: -68.80325317382812 | |
/// | |
/// contains pointB & pointC | |
final polygonABox = boundingBoxes.findId('polygon-a'); | |
expect(polygonABox.minLat, equals(44.7120980759138)); | |
expect(polygonABox.maxLat, equals(44.848567071337264)); | |
expect(polygonABox.minLong, equals(-68.92959594726562)); | |
expect(polygonABox.maxLong, equals(-68.80325317382812)); | |
/// polygon-b | |
/// | |
/// latidude | |
/// min: 44.692576173626684 | |
/// max: 44.84223815129917 | |
/// | |
/// longitude | |
/// min: -68.88908386230469 | |
/// max: -68.66111755371094 | |
/// | |
/// contains pointC & pointD | |
final polygonBBox = boundingBoxes.findId('polygon-b'); | |
expect(polygonBBox.minLat, equals(44.692576173626684)); | |
expect(polygonBBox.maxLat, equals(44.84223815129917)); | |
expect(polygonBBox.minLong, equals(-68.88908386230469)); | |
expect(polygonBBox.maxLong, equals(-68.66111755371094)); | |
/// Test geofencing search | |
/// not fenced | |
final pointA = features.findId<GeoJsonPoint>('point-a'); | |
final resultA = await geo.geofenceSearch(geofences, pointA.geometry); | |
expect(resultA.isEmpty, isTrue); | |
/// fenced by polygon-a | |
final pointB = features.findId<GeoJsonPoint>('point-b'); | |
final resultB = await geo.geofenceSearch(geofences, pointB.geometry); | |
expect(resultB.length, equals(1)); | |
expect(resultB.first.id, equals('polygon-a')); | |
/// fenced by polygon-a & polygon-b | |
final pointC = features.findId<GeoJsonPoint>('point-c'); | |
final resultC = await geo.geofenceSearch(geofences, pointC.geometry); | |
expect(resultC.length, equals(2)); | |
expect(resultC.first.id, equals('polygon-a')); | |
expect(resultC[1].id, equals('polygon-b')); | |
/// fenced by polygon-b | |
final pointD = features.findId<GeoJsonPoint>('point-d'); | |
final resultD = await geo.geofenceSearch(geofences, pointD.geometry); | |
expect(resultD.length, equals(1)); | |
expect(resultD.first.id, equals('polygon-b')); | |
/// within polygon-b bounding box, but not fenced by any polygons | |
final pointE = features.findId<GeoJsonPoint>('point-e'); | |
final resultE = await geo.geofenceSearch(geofences, pointE.geometry); | |
expect(resultE.isEmpty, isTrue); | |
}); | |
}); | |
} | |
/// Convenience extension specifically for `geofence-search-test.json` | |
extension on GeoJsonFeatureCollection { | |
GeoJsonFeature<T> findId<T>(String id) => | |
collection.firstWhere((e) => e.id == id); | |
} | |
/// Convenience extension specifically for `geofence-search-test.json` | |
extension on List<GeoBoundingBox> { | |
GeoBoundingBox findId(String id) => firstWhere((e) => e.feature.id == id); | |
} | |
/// Convenience extension specifically for `geofence-search-test.json` | |
extension on GeoJsonFeature { | |
String get id => properties['id']; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment