Skip to content

Instantly share code, notes, and snippets.

@lukepighetti
Last active January 1, 2021 21:14
Show Gist options
  • Save lukepighetti/442fca7115c752b9a93b025fc04b4c18 to your computer and use it in GitHub Desktop.
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
Display the source blob
Display the rendered blob
Raw
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"stroke": "#555555",
"stroke-width": 2,
"stroke-opacity": 1,
"fill": "#555555",
"fill-opacity": 0.5,
"id": "polygon-a"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-68.92959594726562,
44.83347388333047
],
[
-68.92684936523438,
44.7120980759138
],
[
-68.80325317382812,
44.72283231625664
],
[
-68.8238525390625,
44.848567071337264
],
[
-68.92959594726562,
44.83347388333047
]
]
]
}
},
{
"type": "Feature",
"properties": {
"stroke": "#555555",
"stroke-width": 2,
"stroke-opacity": 1,
"fill": "#555555",
"fill-opacity": 0.5,
"id": "polygon-b"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-68.77647399902344,
44.84223815129917
],
[
-68.88908386230469,
44.78134766441459
],
[
-68.8128662109375,
44.692576173626684
],
[
-68.67828369140625,
44.71258603912906
],
[
-68.66111755371094,
44.79889071584815
],
[
-68.77647399902344,
44.84223815129917
]
]
]
}
},
{
"type": "Feature",
"properties": {
"marker-color": "#7e7e7e",
"marker-size": "medium",
"marker-symbol": "",
"id": "point-a",
"description": "not fenced"
},
"geometry": {
"type": "Point",
"coordinates": [
-68.98040771484375,
44.77842330410185
]
}
},
{
"type": "Feature",
"properties": {
"marker-color": "#7e7e7e",
"marker-size": "medium",
"marker-symbol": "",
"id": "point-b",
"description": "fenced by polygon-a"
},
"geometry": {
"type": "Point",
"coordinates": [
-68.89938354492188,
44.75356026127114
]
}
},
{
"type": "Feature",
"properties": {
"marker-color": "#7e7e7e",
"marker-size": "medium",
"marker-symbol": "",
"id": "point-c",
"description": "fenced by polygon-a & polygon-b"
},
"geometry": {
"type": "Point",
"coordinates": [
-68.83552551269531,
44.7691618526244
]
}
},
{
"type": "Feature",
"properties": {
"marker-color": "#7e7e7e",
"marker-size": "medium",
"marker-symbol": "",
"id": "point-d",
"description": "fenced by polygon-b"
},
"geometry": {
"type": "Point",
"coordinates": [
-68.70780944824219,
44.77939810733011
]
}
},
{
"type": "Feature",
"properties": {
"marker-color": "#7e7e7e",
"marker-size": "medium",
"marker-symbol": "",
"id": "point-e",
"description": "within polygon-b bounding box, but not fenced by any polygons"
},
"geometry": {
"type": "Point",
"coordinates": [
-68.84513854980467,
44.704778133975424
]
}
}
]
}
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)';
}
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