From dafa3cd80aa66a5d42b91fde4087761f9bd239e0 Mon Sep 17 00:00:00 2001 From: Jonas Siedentop Date: Sat, 17 Feb 2024 16:12:12 +0100 Subject: [PATCH] Implement booleanWithin (#167) * document feature booleanWithin * implement boolean within * implement boolean within * add boolean within tests * export boolean features * remove duplicate code * refactor tests * rename import * remove warnings * add source link to readme * add library declaration for turf_boolean * Code review suggestions * . * . * fix typos * fix analyzer warning due to recent dart 3.3.0 release --- README.md | 4 +- lib/boolean.dart | 16 ++ lib/src/booleans/boolean_contains.dart | 130 +++------------ lib/src/booleans/boolean_helper.dart | 216 +++++++++++++++++++++++++ lib/src/booleans/boolean_within.dart | 87 ++++++++++ lib/src/geojson.dart | 15 +- lib/src/geojson.g.dart | 2 +- lib/src/line_to_polygon.dart | 1 - lib/src/nearest_point_on_line.dart | 15 +- test/booleans/within_test.dart | 142 ++++++++++++++++ 10 files changed, 496 insertions(+), 132 deletions(-) create mode 100644 lib/boolean.dart create mode 100644 lib/src/booleans/boolean_helper.dart create mode 100644 lib/src/booleans/boolean_within.dart create mode 100644 test/booleans/within_test.dart diff --git a/README.md b/README.md index 54af62bf..25ba8e6b 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ Any new benchmarks must be named `*_benchmark.dart` and reside in the - [x] [booleanParallel](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_parallel.dart) - [x] [booleanPointInPolygon](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_point_in_polygon.dart) - [x] [booleanPointOnLine](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_point_on_line.dart) -- [ ] booleanWithin +- [x] [booleanWithin](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_within.dart) ### Unit Conversion @@ -250,4 +250,4 @@ Any new benchmarks must be named `*_benchmark.dart` and reside in the - [x] [radiansToLength](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) - [x] [radiansToDegrees](https://github.com/dartclub/turf_dart/blob/main/lib/src/helpers.dart) - [ ] toMercator -- [ ] toWgs84 \ No newline at end of file +- [ ] toWgs84 diff --git a/lib/boolean.dart b/lib/boolean.dart new file mode 100644 index 00000000..aa5a344a --- /dev/null +++ b/lib/boolean.dart @@ -0,0 +1,16 @@ +library turf_boolean; + +export 'src/booleans/boolean_clockwise.dart'; +export 'src/booleans/boolean_concave.dart'; +export 'src/booleans/boolean_contains.dart'; +export 'src/booleans/boolean_crosses.dart'; +export 'src/booleans/boolean_disjoint.dart'; +export 'src/booleans/boolean_equal.dart'; +export 'src/booleans/boolean_intersects.dart'; +// export 'src/booleans/boolean_overlap.dart'; +export 'src/booleans/boolean_parallel.dart'; +export 'src/booleans/boolean_point_in_polygon.dart'; +export 'src/booleans/boolean_point_on_line.dart'; +export 'src/booleans/boolean_touches.dart'; +export 'src/booleans/boolean_valid.dart'; +export 'src/booleans/boolean_within.dart'; diff --git a/lib/src/booleans/boolean_contains.dart b/lib/src/booleans/boolean_contains.dart index eeeb7260..13f77edf 100644 --- a/lib/src/booleans/boolean_contains.dart +++ b/lib/src/booleans/boolean_contains.dart @@ -3,6 +3,7 @@ import 'package:turf/turf.dart'; import 'boolean_point_in_polygon.dart'; import 'boolean_point_on_line.dart'; +import 'boolean_helper.dart'; /// [booleanContains] returns [true] if the second geometry is completely contained /// by the first geometry. @@ -11,162 +12,75 @@ import 'boolean_point_on_line.dart'; /// [booleanContains] returns the exact opposite result of the [booleanWithin]. /// example: /// ```dart -/// var line = LineString(coordinates: [ +/// final line = LineString(coordinates: [ /// Position.of([1, 1]), /// Position.of([1, 2]), /// Position.of([1, 3]), /// Position.of([1, 4]) /// ]); -/// var point = Point(coordinates: Position.of([1, 2])); +/// final point = Point(coordinates: Position.of([1, 2])); /// booleanContains(line, point); /// //=true /// ``` bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { - var geom1 = getGeom(feature1); - var geom2 = getGeom(feature2); + final geom1 = getGeom(feature1); + final geom2 = getGeom(feature2); - var coords1 = (geom1 as GeometryType).coordinates; - var coords2 = (geom2 as GeometryType).coordinates; - final exception = Exception("{feature2 $geom2 geometry not supported}"); + final coords1 = (geom1 as GeometryType).coordinates; + final coords2 = (geom2 as GeometryType).coordinates; if (geom1 is Point) { if (geom2 is Point) { return coords1 == coords2; } else { - throw exception; + throw FeatureNotSupported(geom1, geom2); } } else if (geom1 is MultiPoint) { if (geom2 is Point) { - return _isPointInMultiPoint(geom1, geom2); + return isPointInMultiPoint(geom2, geom1); } else if (geom2 is MultiPoint) { - return _isMultiPointInMultiPoint(geom1, geom2); + return isMultiPointInMultiPoint(geom2, geom1); } else { - throw exception; + throw FeatureNotSupported(geom1, geom2); } } else if (geom1 is LineString) { if (geom2 is Point) { return booleanPointOnLine(geom2, geom1, ignoreEndVertices: true); } else if (geom2 is LineString) { - return _isLineOnLine(geom1, geom2); + return isLineOnLine(geom2, geom1); } else if (geom2 is MultiPoint) { - return _isMultiPointOnLine(geom1, geom2); + return isMultiPointOnLine(geom2, geom1); } else { - throw exception; + throw FeatureNotSupported(geom1, geom2); } } else if (geom1 is Polygon) { if (geom2 is Point) { return booleanPointInPolygon((geom2).coordinates, geom1, ignoreBoundary: true); } else if (geom2 is LineString) { - return _isLineInPoly(geom1, geom2); + return isLineInPolygon(geom2, geom1); } else if (geom2 is Polygon) { return _isPolyInPoly(geom1, geom2); } else if (geom2 is MultiPoint) { - return _isMultiPointInPoly(geom1, geom2); + return isMultiPointInPolygon(geom2, geom1); } else { - throw exception; + throw FeatureNotSupported(geom1, geom2); } } else { - throw exception; + throw FeatureNotSupported(geom1, geom2); } } -bool _isPointInMultiPoint(MultiPoint multiPoint, Point pt) { - for (int i = 0; i < multiPoint.coordinates.length; i++) { - if ((multiPoint.coordinates[i] == pt.coordinates)) { - return true; - } - } - return false; -} - -bool _isMultiPointInMultiPoint(MultiPoint multiPoint1, MultiPoint multiPoint2) { - for (Position coord2 in multiPoint2.coordinates) { - bool match = false; - for (Position coord1 in multiPoint1.coordinates) { - if (coord2 == coord1) { - match = true; - } - } - if (!match) return false; - } - return true; -} - -bool _isMultiPointOnLine(LineString lineString, MultiPoint multiPoint) { - var haveFoundInteriorPoint = false; - for (var coord in multiPoint.coordinates) { - if (booleanPointOnLine(Point(coordinates: coord), lineString, - ignoreEndVertices: true)) { - haveFoundInteriorPoint = true; - } - if (!booleanPointOnLine(Point(coordinates: coord), lineString)) { - return false; - } - } - return haveFoundInteriorPoint; -} - -bool _isMultiPointInPoly(Polygon polygon, MultiPoint multiPoint) { - for (var coord in multiPoint.coordinates) { - if (!booleanPointInPolygon(coord, polygon, ignoreBoundary: true)) { - return false; - } - } - return true; -} - -bool _isLineOnLine(LineString lineString1, LineString lineString2) { - var haveFoundInteriorPoint = false; - for (Position coord in lineString2.coordinates) { - if (booleanPointOnLine( - Point(coordinates: coord), - lineString1, - ignoreEndVertices: true, - )) { - haveFoundInteriorPoint = true; - } - if (!booleanPointOnLine( - Point(coordinates: coord), - lineString1, - ignoreEndVertices: false, - )) { - return false; - } - } - return haveFoundInteriorPoint; -} - -bool _isLineInPoly(Polygon polygon, LineString linestring) { - var polyBbox = bbox(polygon); - var lineBbox = bbox(linestring); - if (!_doBBoxesOverlap(polyBbox, lineBbox)) { - return false; - } - for (var i = 0; i < linestring.coordinates.length - 1; i++) { - var midPoint = - midpointRaw(linestring.coordinates[i], linestring.coordinates[i + 1]); - if (booleanPointInPolygon( - midPoint, - polygon, - ignoreBoundary: true, - )) { - return true; - } - } - return false; -} - /// Is Polygon2 in Polygon1 /// Only takes into account outer rings bool _isPolyInPoly(GeoJSONObject geom1, GeoJSONObject geom2) { - var poly1Bbox = bbox(geom1); - var poly2Bbox = bbox(geom2); + final poly1Bbox = bbox(geom1); + final poly2Bbox = bbox(geom2); if (!_doBBoxesOverlap(poly1Bbox, poly2Bbox)) { return false; } - for (var ring in (geom2 as GeometryType).coordinates) { - for (var coord in ring) { + for (final ring in (geom2 as GeometryType).coordinates) { + for (final coord in ring) { if (!booleanPointInPolygon(coord, geom1)) { return false; } diff --git a/lib/src/booleans/boolean_helper.dart b/lib/src/booleans/boolean_helper.dart new file mode 100644 index 00000000..9ecea583 --- /dev/null +++ b/lib/src/booleans/boolean_helper.dart @@ -0,0 +1,216 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/src/bbox.dart'; + +import 'boolean_point_on_line.dart'; +import 'boolean_point_in_polygon.dart'; + +class FeatureNotSupported implements Exception { + final GeometryObject geometry1; + final GeometryObject geometry2; + + FeatureNotSupported(this.geometry1, this.geometry2); + + @override + String toString() => + "feature geometry not supported ($geometry1, $geometry2)."; +} + +bool isPointInMultiPoint(Point point, MultiPoint multipoint) { + return multipoint.coordinates + .any((position) => position == point.coordinates); +} + +bool isPointOnLine(Point point, LineString line) { + return booleanPointOnLine(point, line, ignoreEndVertices: true); +} + +bool isPointInPolygon(Point point, Polygon polygon) { + return booleanPointInPolygon( + point.coordinates, + polygon, + ignoreBoundary: true, + ); +} + +bool isPointInMultiPolygon(Point point, MultiPolygon polygon) { + return booleanPointInPolygon( + point.coordinates, + polygon, + ignoreBoundary: true, + ); +} + +bool isMultiPointInMultiPoint(MultiPoint points1, MultiPoint points2) { + return points1.coordinates.every( + (point1) => points2.coordinates.any( + (point2) => point1 == point2, + ), + ); +} + +bool isMultiPointOnLine(MultiPoint points, LineString line) { + final allPointsOnLine = points.coordinates.every( + (point) => booleanPointOnLine( + Point(coordinates: point), + line, + ), + ); + if (allPointsOnLine) { + final anyInteriorPoint = points.coordinates.any( + (point) => booleanPointOnLine( + Point(coordinates: point), + line, + ignoreEndVertices: true, + ), + ); + + if (anyInteriorPoint) { + return true; + } + } + return false; +} + +bool isMultiPointInPolygon(MultiPoint points, Polygon polygon) => + _isMultiPointInGeoJsonPolygon(points, polygon); + +bool isMultiPointInMultiPolygon(MultiPoint points, MultiPolygon polygon) => + _isMultiPointInGeoJsonPolygon(points, polygon); + +bool _isMultiPointInGeoJsonPolygon(MultiPoint points, GeoJSONObject polygon) { + final allPointsInsideThePolygon = points.coordinates.every( + (point) => booleanPointInPolygon( + point, + polygon, + ), + ); + + if (allPointsInsideThePolygon) { + final onePointNotOnTheBorder = points.coordinates.any( + (point) => booleanPointInPolygon( + point, + polygon, + ignoreBoundary: true, + ), + ); + + if (onePointNotOnTheBorder) { + return true; + } + } + return false; +} + +bool isLineOnLine(LineString line1, LineString line2) { + return line1.coordinates.every((point) { + return booleanPointOnLine( + Point(coordinates: point), + line2, + ); + }); +} + +bool isLineInPolygon(LineString line, Polygon polygon) => + _isLineInGeoJsonPolygon(line, polygon); + +bool isLineInMultiPolygon(LineString line, MultiPolygon polygon) => + _isLineInGeoJsonPolygon(line, polygon); + +bool _isLineInGeoJsonPolygon(LineString line, GeoJSONObject polygon) { + final boundingBoxOfPolygon = bbox(polygon); + final boundingBoxOfLine = bbox(line); + + if (!_doBBoxesOverlap(boundingBoxOfPolygon, boundingBoxOfLine)) { + return false; + } + + final allPointsInsideThePolygon = line.coordinates.every( + (position) => booleanPointInPolygon( + position, + polygon, + ), + ); + + if (allPointsInsideThePolygon) { + if (_anyLinePointNotOnBoundary(line, polygon)) { + return true; + } + + if (_isLineCrossingThePolygon(line, polygon)) { + return true; + } + } + + return false; +} + +bool _anyLinePointNotOnBoundary(LineString line, GeoJSONObject polygon) { + return line.coordinates.any( + (position) => booleanPointInPolygon( + position, + polygon, + ignoreBoundary: true, + ), + ); +} + +bool _isLineCrossingThePolygon(LineString line, GeoJSONObject polygon) { + List midpoints = List.generate( + line.coordinates.length - 1, + (index) => _getMidpoint( + line.coordinates[index], + line.coordinates[index + 1], + ), + ); + + return midpoints.any( + (position) => booleanPointInPolygon( + position, + polygon, + ignoreBoundary: true, + ), + ); +} + +bool _doBBoxesOverlap(BBox bbox1, BBox bbox2) { + if (bbox1[0]! > bbox2[0]!) return false; + if (bbox1[2]! < bbox2[2]!) return false; + if (bbox1[1]! > bbox2[1]!) return false; + if (bbox1[3]! < bbox2[3]!) return false; + return true; +} + +Position _getMidpoint(Position position1, Position position2) { + return Position( + (position1.lng + position2.lng) / 2, + (position1.lat + position2.lat) / 2, + ); +} + +bool isPolygonInPolygon(Polygon polygon1, Polygon polygon2) => + _isPolygonInGeoJsonPolygon(polygon1, polygon2); + +bool isPolygonInMultiPolygon(Polygon polygon1, MultiPolygon polygon2) => + _isPolygonInGeoJsonPolygon(polygon1, polygon2); + +bool _isPolygonInGeoJsonPolygon( + Polygon polygon1, + GeoJSONObject polygon2, +) { + final boundingBoxOfPolygon1 = bbox(polygon1); + final boundingBoxOfPolygon2 = bbox(polygon2); + if (!_doBBoxesOverlap(boundingBoxOfPolygon2, boundingBoxOfPolygon1)) { + return false; + } + + final positions = polygon1.coordinates[0]; + final anyPointNotInPolygon = positions.any( + (point) => !booleanPointInPolygon(point, polygon2), + ); + + if (anyPointNotInPolygon) { + return false; + } + + return true; +} diff --git a/lib/src/booleans/boolean_within.dart b/lib/src/booleans/boolean_within.dart new file mode 100644 index 00000000..0b14d8f7 --- /dev/null +++ b/lib/src/booleans/boolean_within.dart @@ -0,0 +1,87 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/src/invariant.dart'; + +import 'boolean_helper.dart'; + +/// Returns [true] if the first [GeoJSONObject] is completely within the second [GeoJSONObject]. +/// The interiors of both geometries must intersect and, the interior and boundary +/// of the primary (geometry a) must not intersect the exterior of the secondary +/// (geometry b). [booleanWithin] returns the exact opposite result of [booleanContains]. +/// +/// +/// example: +/// ```dart +/// final point = Point(coordinates: [1, 2]); +/// final line = LineString( +/// coordinates: [ +/// Position.of([1, 1]), +/// Position.of([1, 2]), +/// Position.of([1, 3]), +/// Position.of([1, 4]) +/// ], +/// ); +/// booleanWithin(point, line); +/// //=true +/// ``` +bool booleanWithin( + GeoJSONObject feature1, + GeoJSONObject feature2, +) { + final geom1 = getGeom(feature1); + final geom2 = getGeom(feature2); + + switch (geom1.runtimeType) { + case Point: + final point = geom1 as Point; + switch (geom2.runtimeType) { + case MultiPoint: + return isPointInMultiPoint(point, geom2 as MultiPoint); + case LineString: + return isPointOnLine(point, geom2 as LineString); + case Polygon: + return isPointInPolygon(point, geom2 as Polygon); + case MultiPolygon: + return isPointInMultiPolygon(point, geom2 as MultiPolygon); + default: + throw FeatureNotSupported(geom1, geom2); + } + case MultiPoint: + final multipoint = geom1 as MultiPoint; + switch (geom2.runtimeType) { + case MultiPoint: + return isMultiPointInMultiPoint(multipoint, geom2 as MultiPoint); + case LineString: + return isMultiPointOnLine(multipoint, geom2 as LineString); + case Polygon: + return isMultiPointInPolygon(multipoint, geom2 as Polygon); + case MultiPolygon: + return isMultiPointInMultiPolygon(multipoint, geom2 as MultiPolygon); + default: + throw FeatureNotSupported(geom1, geom2); + } + case LineString: + final line = geom1 as LineString; + switch (geom2.runtimeType) { + case LineString: + return isLineOnLine(line, geom2 as LineString); + case Polygon: + return isLineInPolygon(line, geom2 as Polygon); + case MultiPolygon: + return isLineInMultiPolygon(line, geom2 as MultiPolygon); + default: + throw FeatureNotSupported(geom1, geom2); + } + case Polygon: + final polygon = geom1 as Polygon; + switch (geom2.runtimeType) { + case Polygon: + return isPolygonInPolygon(polygon, geom2 as Polygon); + case MultiPolygon: + return isPolygonInMultiPolygon(polygon, geom2 as MultiPolygon); + default: + throw FeatureNotSupported(geom1, geom2); + } + default: + throw FeatureNotSupported(geom1, geom2); + } +} diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart index 619fe127..c967e74f 100644 --- a/lib/src/geojson.dart +++ b/lib/src/geojson.dart @@ -210,9 +210,7 @@ class Position extends CoordinateType { ]); /// Position.of([, , ]) - Position.of(List list) - : assert(list.length >= 2 && list.length <= 3), - super(list); + Position.of(super.list) : assert(list.length >= 2 && list.length <= 3); factory Position.fromJson(List list) => Position.of(list); @@ -269,7 +267,7 @@ class Position extends CoordinateType { int get hashCode => Object.hashAll(_items); @override - bool operator ==(dynamic other) => other is Position + bool operator ==(Object other) => other is Position ? lng == other.lng && lat == other.lat && alt == other.alt : false; } @@ -324,9 +322,7 @@ class BBox extends CoordinateType { ]); /// Position.of([, , ]) - BBox.of(List list) - : assert(list.length == 4 || list.length == 6), - super(list); + BBox.of(super.list) : assert(list.length == 4 || list.length == 6); factory BBox.fromJson(List list) => BBox.of(list); @@ -392,8 +388,7 @@ class BBox extends CoordinateType { } abstract class GeometryObject extends GeoJSONObject { - GeometryObject.withType(GeoJSONObjectType type, {BBox? bbox}) - : super.withType(type, bbox: bbox); + GeometryObject.withType(super.type, {super.bbox}) : super.withType(); static GeometryObject deserialize(Map json) { return json['type'] == 'GeometryCollection' || @@ -684,7 +679,7 @@ class Feature extends GeoJSONObject { int get hashCode => Object.hash(type, id); @override - bool operator ==(dynamic other) => other is Feature ? id == other.id : false; + bool operator ==(Object other) => other is Feature ? id == other.id : false; @override Map toJson() => super.serialize({ diff --git a/lib/src/geojson.g.dart b/lib/src/geojson.g.dart index d8ae8cdf..7a07462c 100644 --- a/lib/src/geojson.g.dart +++ b/lib/src/geojson.g.dart @@ -126,7 +126,7 @@ Map _$MultiPolygonToJson(MultiPolygon instance) => Map _$GeometryCollectionToJson(GeometryCollection instance) => { - 'type': _$GeoJSONObjectTypeEnumMap[instance.type], + 'type': _$GeoJSONObjectTypeEnumMap[instance.type]!, 'bbox': instance.bbox?.toJson(), 'geometries': instance.geometries.map((e) => e.toJson()).toList(), }; diff --git a/lib/src/line_to_polygon.dart b/lib/src/line_to_polygon.dart index bf537dad..59e01527 100644 --- a/lib/src/line_to_polygon.dart +++ b/lib/src/line_to_polygon.dart @@ -60,7 +60,6 @@ Feature lineToPolygon( ...list, ...currentGeometry.coordinates .map((e) => e.map((p) => p.clone()).toList()) - .toList() ]; } else { throw Exception("$currentGeometry type is not supperted"); diff --git a/lib/src/nearest_point_on_line.dart b/lib/src/nearest_point_on_line.dart index 4656bef1..1e037be1 100644 --- a/lib/src/nearest_point_on_line.dart +++ b/lib/src/nearest_point_on_line.dart @@ -37,18 +37,13 @@ class _NearestMulti extends _Nearest { final int localIndex; _NearestMulti({ - required Point point, - required num distance, - required int index, + required super.point, + required super.distance, + required super.index, required this.localIndex, - required num location, + required super.location, required this.line, - }) : super( - point: point, - distance: distance, - index: index, - location: location, - ); + }); @override Feature toFeature() { diff --git a/test/booleans/within_test.dart b/test/booleans/within_test.dart new file mode 100644 index 00000000..a37101b3 --- /dev/null +++ b/test/booleans/within_test.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import 'package:turf/src/booleans/boolean_within.dart'; + +void main() { + group('within - true', () { + loadGeoJsonFiles('./test/examples/booleans/within/true', (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + test(path, () => expect(booleanWithin(feature1, feature2), true)); + }); + }); + + group('within - false', () { + loadGeoJsonFiles('./test/examples/booleans/within/false', (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + test(path, () => expect(booleanWithin(feature1, feature2), false)); + }); + }); + + group('within', () { + loadGeoJson( + './test/examples/booleans/within/true/MultiPolygon/MultiPolygon/skip-multipolygon-within-multipolygon.geojson', + (path, geoJson) { + final feature1 = (geoJson as FeatureCollection).features[0]; + final feature2 = geoJson.features[1]; + + test( + 'FeatureNotSupported', + () => expect( + () => booleanWithin(feature1, feature2), + throwsA(isA()), + ), + ); + }); + + test('within - point in multipolygon with hole', () { + loadGeoJson( + './test/examples/booleans/point_in_polygon/in/multipoly-with-hole.geojson', + (path, geoJson) { + final multiPolygon = (geoJson as Feature); + final pointInHole = point([-86.69208526611328, 36.20373274711739]); + final pointInPolygon = point([-86.72229766845702, 36.20258997094334]); + final pointInSecondPolygon = + point([-86.75079345703125, 36.18527313913089]); + + expect(booleanWithin(pointInHole, multiPolygon), false, + reason: "point in hole"); + expect(booleanWithin(pointInPolygon, multiPolygon), true, + reason: "point in polygon"); + expect(booleanWithin(pointInSecondPolygon, multiPolygon), true, + reason: "point outside polygon"); + }); + }); + + test("within - point in polygon", () { + final simplePolygon = polygon([ + [ + [0, 0], + [0, 100], + [100, 100], + [100, 0], + [0, 0], + ], + ]); + final pointIn = point([50, 50]); + final pointOut = point([140, 150]); + + expect(booleanWithin(pointIn, simplePolygon), true, + reason: "point inside polygon"); + expect(booleanWithin(pointOut, simplePolygon), false, + reason: "point outside polygon"); + + final concavePolygon = polygon([ + [ + [0, 0], + [50, 50], + [0, 100], + [100, 100], + [100, 0], + [0, 0], + ], + ]); + + final pointInConcave = point([75, 75]); + final pointOutConcave = point([25, 50]); + + expect(booleanWithin(pointInConcave, concavePolygon), true, + reason: "point inside concave polygon"); + expect(booleanWithin(pointOutConcave, concavePolygon), false, + reason: "point outside concave polygon"); + }); + }); +} + +void loadGeoJson( + String path, void Function(String path, GeoJSONObject geoJson) test) { + final file = File(path); + final content = file.readAsStringSync(); + final geoJson = GeoJSONObject.fromJson(jsonDecode(content)); + test(file.path, geoJson); +} + +void loadGeoJsonFiles( + String path, void Function(String path, GeoJSONObject geoJson) test) { + final testDirectory = Directory(path); + + for (final file in testDirectory.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + if (file.path.contains('skip')) continue; + + final content = file.readAsStringSync(); + final geoJson = GeoJSONObject.fromJson(jsonDecode(content)); + test(file.path, geoJson); + } + } +} + +Point point(List coordinates) { + return Point(coordinates: Position.of(coordinates)); +} + +Feature polygon(List>> coordinates) { + return Feature( + geometry: Polygon(coordinates: coordinates.toPositions()), + ); +} + +extension PointsExtension on List> { + List toPositions() => + map((position) => Position.of(position)).toList(growable: false); +} + +extension PolygonPointsExtensions on List>> { + List> toPositions() => + map((element) => element.toPositions()).toList(growable: false); +}