diff --git a/lib/src/bbox.dart b/lib/src/bbox.dart index a7d37ab..933009c 100644 --- a/lib/src/bbox.dart +++ b/lib/src/bbox.dart @@ -9,10 +9,12 @@ BBox bbox(GeoJSONObject geoJson, {bool recompute = false}) { } var result = BBox.named( - lat1: double.infinity, + // min x & y lng1: double.infinity, - lat2: double.negativeInfinity, + lat1: double.infinity, + // max x & y lng2: double.negativeInfinity, + lat2: double.negativeInfinity, ); coordEach( diff --git a/lib/src/booleans/boolean_overlap.dart b/lib/src/booleans/boolean_overlap.dart new file mode 100644 index 0000000..6a2eb7b --- /dev/null +++ b/lib/src/booleans/boolean_overlap.dart @@ -0,0 +1,147 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/src/invariant.dart'; +import 'package:turf/src/line_intersect.dart'; +import 'package:turf/src/line_overlap.dart'; +import 'package:turf_equality/turf_equality.dart'; + +/// Compares two geometries of the same dimension and returns [true] if their +/// intersection Set results in a geometry different from both but of the same +/// dimension. It applies to [Polygon]/[Polygon], [LineString]/[LineString], [MultiPoint]/ +/// [MultiPoint], [MultiLineString]/[MultiLineString] and [MultiPolygon]/[MultiPolygon]. +/// In other words, it returns [true] if the two geometries overlap, provided that +/// neither completely contains the other. +/// Takes [feature1] and [feature2] which could be [Feature]<[LineString]| +/// [MultiLineString]|[Polygon]|[MultiPolygon]> +/// example +/// ```dart +/// var poly1 = Polygon( +/// coordinates: [ +/// [ +/// Position.of([0, 0]), +/// Position.of([0, 5]), +/// Position.of([5, 5]), +/// Position.of([5, 0]), +/// Position.of([0, 0]) +/// ] +/// ], +/// ); +/// var poly2 = Polygon( +/// coordinates: [ +/// [ +/// Position.of([1, 1]), +/// Position.of([1, 6]), +/// Position.of([6, 6]), +/// Position.of([6, 1]), +/// Position.of([1, 1]) +/// ] +/// ], +/// ); +/// var poly3 = Polygon( +/// coordinates: [ +/// [ +/// Position.of([10, 10]), +/// Position.of([10, 15]), +/// Position.of([15, 15]), +/// Position.of([15, 10]), +/// Position.of([10, 10]) +/// ] +/// ], +/// ); +/// booleanOverlap(poly1, poly2); +/// //=true +/// booleanOverlap(poly2, poly3); +/// //=false +/// ``` +bool booleanOverlap(GeoJSONObject feature1, GeoJSONObject feature2) { + var geom1 = getGeom(feature1); + var geom2 = getGeom(feature2); + + if ((feature1 is MultiPoint && feature2 is! MultiPoint) || + ((feature1 is LineString || feature1 is MultiLineString) && + feature2 is! LineString && + feature2 is! MultiLineString) || + ((feature1 is Polygon || feature1 is MultiPolygon) && + feature2 is! Polygon && + feature2 is! MultiPolygon)) { + throw Exception("features must be of the same type"); + } + if (feature1 is Point) throw Exception("Point geometry not supported"); + + // features must be not equal + var equality = Equality(precision: 6); + if (equality.compare(feature1, feature2)) { + return false; + } + + var overlap = 0; + + if (geom1 is MultiPoint) { + for (var i = 0; i < geom1.coordinates.length; i++) { + for (var j = 0; j < (geom2 as MultiPoint).coordinates.length; j++) { + if (geom1.coordinates[i] == geom2.coordinates[j]) { + return true; + } + } + } + return false; + } else if (feature1 is MultiLineString) { + segmentEach( + feature1, + ( + Feature currentSegment, + int featureIndex, + int? multiFeatureIndex, + int? geometryIndex, + int segmentIndex, + ) { + segmentEach( + feature2, + ( + Feature currentSegment1, + int featureIndex, + int? multiFeatureIndex, + int? geometryIndex, + int segmentIndex, + ) { + if (lineOverlap(currentSegment, currentSegment1) + .features + .isNotEmpty) { + overlap++; + } + }, + ); + }, + ); + } else if (feature1 is Polygon || feature1 is MultiPolygon) { + segmentEach( + feature1, + ( + Feature currentSegment, + int featureIndex, + int? multiFeatureIndex, + int? geometryIndex, + int segmentIndex, + ) { + segmentEach( + feature2, + ( + Feature currentSegment1, + int featureIndex, + int? multiFeatureIndex, + int? geometryIndex, + int segmentIndex, + ) { + if (lineIntersect(currentSegment, currentSegment1) + .features + .isNotEmpty) { + overlap++; + } + }, + ); + }, + ); + } + + return overlap > 0; +} diff --git a/lib/src/line_overlap.dart b/lib/src/line_overlap.dart new file mode 100644 index 0000000..c901c75 --- /dev/null +++ b/lib/src/line_overlap.dart @@ -0,0 +1,195 @@ +import 'package:rbush/rbush.dart'; +import 'package:turf/bbox.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/nearest_point_on_line.dart'; +import 'package:turf/src/booleans/boolean_point_on_line.dart'; +import 'package:turf/src/invariant.dart'; +import 'package:turf/src/meta/feature.dart'; +import 'package:turf_equality/turf_equality.dart'; + +/// Takes any [LineString] or [Polygon] and returns the overlapping [LineString]s +/// between both [Feature]s. [line1] is a [Feature]<[LineString]|[MultiLineString] +/// |[Polygon]|[MultiPolygon]> or any [LineString] or [Polygon], [line2] is a +/// [Feature]<[LineString]|[MultiLineString]|[Polygon]|[MultiPolygon]> or any +/// [LineString] or [Polygon]. [tolerance=0] Tolerance distance to match +/// overlapping line segments (in kilometers) returns a [FeatureCollection]<[LineString]> +/// lines(s) that are overlapping between both [Feature]s. +/// example +/// ```dart +/// var line1 = LineString( +/// coordinates: [ +/// Position.of([115, -35]), +/// Position.of([125, -30]), +/// Position.of([135, -30]), +/// Position.of([145, -35]) +/// ], +/// ); +/// var line2 = LineString( +/// coordinates: [ +/// Position.of([115, -25]), +/// Position.of([125, -30]), +/// Position.of([135, -30]), +/// Position.of([145, -25]) +/// ], +/// ); +/// var overlapping = lineOverlap(line1, line2); +/// //addToMap +/// var addToMap = [line1, line2, overlapping] +///``` +FeatureCollection lineOverlap( + GeoJSONObject line1, GeoJSONObject line2, + {num tolerance = 0}) { + RBushBox _toRBBox(Feature feature) { + return RBushBox.fromList(bbox(feature).toList()); + } + + // Containers + var features = >[]; + + // Create Spatial Index + var tree = RBushBase>( + maxEntries: 4, + getMinX: (Feature feature) => bbox(feature).lng1.toDouble(), + getMinY: (Feature feature) => bbox(feature).lat1.toDouble(), + toBBox: _toRBBox, + ); + + FeatureCollection line = lineSegment(line1); + tree.load(line.features); + Feature? overlapSegment; + List> additionalSegments = []; + + // Line Intersection + + // Iterate over line segments + segmentEach(line2, (Feature currentSegment, int featureIndex, + int? multiFeatureIndex, int? geometryIndex, int segmentIndex) { + bool doesOverlap = false; + var list = tree.search(_toRBBox(currentSegment)); + // Iterate over each segments which falls within the same bounds + featureEach(FeatureCollection(features: list), + (Feature currentFeature, int featureIndex) { + if (!doesOverlap) { + var coords = getCoords(currentSegment) as List; + var coordsMatch = getCoords(currentFeature) as List; + + coords.sort(((a, b) { + return a.lng < b.lng + ? -1 + : a.lng > b.lng + ? 1 + : 0; + })); + + coordsMatch.sort(((a, b) { + return a.lng < b.lng + ? -1 + : a.lng > b.lng + ? 1 + : 0; + })); + + Equality eq = Equality(); + // Segment overlaps feature - with dummy LineStrings just to use eq. + if (eq.compare(LineString(coordinates: coords), + LineString(coordinates: coordsMatch))) { + doesOverlap = true; + // Overlaps already exists - only append last coordinate of segment + if (overlapSegment != null) { + overlapSegment = concatSegment(overlapSegment!, currentSegment) ?? + overlapSegment; + } else { + overlapSegment = currentSegment; + } + // Match segments which don't share nodes (Issue #901) + } else if (tolerance == 0 + ? booleanPointOnLine(Point(coordinates: coords[0]), currentFeature.geometry as LineString) && + booleanPointOnLine(Point(coordinates: coords[1]), + currentFeature.geometry as LineString) + : nearestPointOnLine(currentFeature.geometry as LineString, Point(coordinates: coords[0])) + .properties!['dist'] <= + tolerance && + nearestPointOnLine(currentFeature.geometry as LineString, Point(coordinates: coords[1])) + .properties!['dist'] <= + tolerance) { + doesOverlap = true; + if (overlapSegment != null) { + overlapSegment = concatSegment(overlapSegment!, currentSegment) ?? + overlapSegment; + } else { + overlapSegment = currentSegment; + } + } else if (tolerance == 0 + ? booleanPointOnLine(Point(coordinates: coordsMatch[0]), + currentSegment.geometry as LineString) && + booleanPointOnLine(Point(coordinates: coordsMatch[1]), + currentSegment.geometry as LineString) + : nearestPointOnLine(currentSegment.geometry as LineString, + Point(coordinates: coordsMatch[0])) + .properties!['dist'] <= + tolerance && + nearestPointOnLine( + currentSegment.geometry as LineString, Point(coordinates: coordsMatch[1])) + .properties!['dist'] <= + tolerance) { + // Do not define doesOverlap = true since more matches can occur + // within the same segment + // doesOverlaps = true; + if (overlapSegment != null) { + Feature? combinedSegment = concatSegment( + overlapSegment!, currentFeature as Feature); + if (combinedSegment != null) { + overlapSegment = combinedSegment; + } else { + additionalSegments.add(currentFeature); + } + } else { + overlapSegment = currentFeature as Feature; + } + } + } + }); + + // Segment doesn't overlap - add overlaps to results & reset + if (doesOverlap == false && overlapSegment != null) { + features.add(overlapSegment!); + if (additionalSegments.isNotEmpty) { + features.addAll(additionalSegments); + additionalSegments = []; + } + overlapSegment = null; + } + }); + // Add last segment if exists + if (overlapSegment != null) features.add(overlapSegment!); + + return FeatureCollection(features: features); +} + +Feature? concatSegment( + Feature line, + Feature segment, +) { + var coords = getCoords(segment) as List; + var lineCoords = getCoords(line) as List; + Position start = lineCoords[0]; + Position end = lineCoords[lineCoords.length - 1]; + List positions = (line.geometry as LineString).clone().coordinates; + + if (coords[0] == start) { + positions.insert(0, coords[1]); + } else if (coords[0] == end) { + positions.add(coords[1]); + } else if (coords[1] == start) { + positions.insert(0, coords[0]); + } else if (coords[1] == end) { + positions.add(coords[0]); + } else { + return null; + } // If the overlap leaves the segment unchanged, return null so that this can be + // identified. + + // Otherwise return the mutated line. + return Feature(geometry: LineString(coordinates: positions)); +} diff --git a/test/booleans/overlap_test.dart b/test/booleans/overlap_test.dart new file mode 100644 index 0000000..354df3b --- /dev/null +++ b/test/booleans/overlap_test.dart @@ -0,0 +1,150 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/booleans/boolean_overlap.dart'; + +void main() { + group( + 'boolean-overlap', + () { + test("turf-boolean-overlap-trues", () { + // True Fixtures + Directory dir = Directory('./test/examples/booleans/overlap/true'); + for (var file in dir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + var inSource = file.readAsStringSync(); + var inGeom = GeoJSONObject.fromJson(jsonDecode(inSource)); + var feature1 = (inGeom as FeatureCollection).features[0]; + var feature2 = inGeom.features[1]; + var result = booleanOverlap(feature1, feature2); + expect(result, true); + } + } + }); + test( + "turf-boolean-overlap - falses", + () { + // True Fixtures + Directory dir1 = Directory('./test/examples/booleans/overlap/false'); + for (var file in dir1.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + var inSource = file.readAsStringSync(); + var inGeom = GeoJSONObject.fromJson(jsonDecode(inSource)); + var feature1 = (inGeom as FeatureCollection).features[0]; + var feature2 = inGeom.features[1]; + var result = booleanOverlap(feature1, feature2); + expect(result, false); + } + } + }, + ); + + var pt = Point(coordinates: Position.of([9, 50])); + var line1 = LineString( + coordinates: [ + Position.of([7, 50]), + Position.of([8, 50]), + Position.of([9, 50]), + ], + ); + var line2 = LineString( + coordinates: [ + Position.of([8, 50]), + Position.of([9, 50]), + Position.of([10, 50]), + ], + ); + var poly1 = Polygon( + coordinates: [ + [ + Position.of([8.5, 50]), + Position.of([9.5, 50]), + Position.of([9.5, 49]), + Position.of([8.5, 49]), + Position.of([8.5, 50]), + ], + ], + ); + var poly2 = Polygon( + coordinates: [ + [ + Position.of([8, 50]), + Position.of([9, 50]), + Position.of([9, 49]), + Position.of([8, 49]), + Position.of([8, 50]), + ], + ], + ); + var poly3 = Polygon( + coordinates: [ + [ + Position.of([10, 50]), + Position.of([10.5, 50]), + Position.of([10.5, 49]), + Position.of([10, 49]), + Position.of([10, 50]), + ], + ], + ); + var multiline1 = MultiLineString( + coordinates: [ + [ + Position.of([8, 50]), + Position.of([9, 50]), + Position.of([7, 50]), + ], + ], + ); + var multipoly1 = MultiPolygon( + coordinates: [ + [ + [ + Position.of([8.5, 50]), + Position.of([9.5, 50]), + Position.of([9.5, 49]), + Position.of([8.5, 49]), + Position.of([8.5, 50]), + ], + ], + ], + ); + + test( + "turf-boolean-overlap -- geometries", + () { + expect(booleanOverlap(line1, line2), true); + expect(booleanOverlap(poly1, poly2), true); + var z = booleanOverlap(poly1, poly3); + expect(z, isFalse); + }, + ); + + test( + "turf-boolean-overlap -- throws", + () { + // t.throws(() => overlap(null, line1), /feature1 is required/, 'missing feature1'); + // t.throws(() => overlap(line1, null), /feature2 is required/, 'missing feature2'); + +//'different types', + expect( + () => booleanOverlap( + poly1, + line1, + ), + throwsA(isA())); +// "geometry not supported" + + expect(() => booleanOverlap(pt, pt), throwsA(isA())); + // "supports line and multiline comparison" + var x = booleanOverlap(line1, multiline1); + expect(() => booleanOverlap(line1, multiline1), x); + var y = booleanOverlap(poly1, multipoly1); + expect(() => booleanOverlap(poly1, multipoly1), y); + }, + ); + }, + ); +} diff --git a/test/components/line_overlap_test.dart b/test/components/line_overlap_test.dart new file mode 100644 index 0000000..ac51371 --- /dev/null +++ b/test/components/line_overlap_test.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/meta.dart'; +import 'package:turf/src/line_overlap.dart'; +import 'package:turf/src/meta/feature.dart'; +import 'package:turf_equality/turf_equality.dart'; + +void main() { + FeatureCollection colorize(features, {color = "#F00", width = 25}) { + var results = []; + featureEach( + features, + (Feature currentFeature, int featureIndex) { + currentFeature.properties = { + 'stroke': color, + 'fill': color, + "stroke-width": width + }; + results.add(currentFeature); + }, + ); + return FeatureCollection(features: results); + } + + group( + 'line_overlap function', + () { + // fixtures = fixtures.filter(({name}) => name.includes('#901')); + + var inDir = Directory('./test/examples/line_overlap/in'); + for (var file in inDir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + // test( + // file.path, + // () { + // var inSource = file.readAsStringSync(); + // var inGeom = GeoJSONObject.fromJson(jsonDecode(inSource)) + // as FeatureCollection; + + // String outPath = + // "./${file.uri.pathSegments.sublist(0, file.uri.pathSegments.length - 2).join('/')}/out/${file.uri.pathSegments.last}"; + + // var outSource = File(outPath).readAsStringSync(); + + // var outGeom = GeoJSONObject.fromJson(jsonDecode(outSource)); + + // Equality eq = Equality(); + // FeatureCollection shared = colorize( + // lineOverlap( + // inGeom.features.first, + // inGeom.features.last, + // ), + // color: "#0F0"); + // // print(shared.features); + // // shared.features.forEach( + // // (element) { + // // print(element.geometry); + // // (element.geometry as GeometryType) + // // .coordinates + // // .forEach((e) => print("${e.lng}-${e.lat}")); + // // }, + // // ); + // FeatureCollection results = FeatureCollection(features: [ + // ...shared.features, + // inGeom.features.first, + // inGeom.features.last + // ]); + // // print(results.features.length); + // expect(eq.compare(results, outGeom), isTrue); + // }, + // ); + } + } + test( + "turf-line-overlap - Geometry Object", + () { + var line1 = LineString( + coordinates: [ + Position.of([115, -35]), + Position.of([125, -30]), + Position.of([135, -30]), + Position.of([145, -35]), + ], + ); + var line2 = LineString( + coordinates: [ + Position.of([135, -30]), + Position.of([145, -35]), + ], + ); + + expect(lineOverlap(line1, line2).features.isNotEmpty, isTrue); + }, + ); + + test( + "turf-line-overlap - multiple segments on same line", + () { + var line1 = LineString( + coordinates: [ + Position.of([0, 1]), + Position.of([1, 1]), + Position.of([1, 0]), + Position.of([2, 0]), + Position.of([2, 1]), + Position.of([3, 1]), + Position.of([3, 0]), + Position.of([4, 0]), + Position.of([4, 1]), + Position.of([4, 0]), + ], + ); + var line2 = LineString( + coordinates: [ + Position.of([0, 0]), + Position.of([6, 0]), + ], + ); + // multiple segments on same line + + expect(lineOverlap(line1, line2).features.length == 2, true); + // multiple segments on same line - swapped order + expect(lineOverlap(line2, line1).features.length == 2, true); + }, + ); + }, + ); +}