diff --git a/README.md b/README.md index 25ba8e6b..e86aaf92 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Any new benchmarks must be named `*_benchmark.dart` and reside in the - [ ] lineArc - [ ] lineChunk - [ ] [lineIntersect](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_intersect.dart) -- [ ] lineOverlap +- [x] [lineOverlap](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_overlap.dart) - [x] [lineSegment](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) - [x] [lineSlice](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_slice.dart) - [ ] lineSliceAlong @@ -233,7 +233,7 @@ Any new benchmarks must be named `*_benchmark.dart` and reside in the - [x] [booleanDisjoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_disjoint.dart) - [x] [booleanEqual](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_equal.dart) - [x] [booleanIntersects](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_intersects.dart) -- [ ] booleanOverlap +- [x] [booleanOverlap](https://github.com/dartclub/turf_dart/blob/main/lib/src/booleans/boolean_overlap.dart) - [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) diff --git a/lib/boolean.dart b/lib/boolean.dart index aa5a344a..87e99c34 100644 --- a/lib/boolean.dart +++ b/lib/boolean.dart @@ -7,7 +7,7 @@ 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_overlap.dart'; export 'src/booleans/boolean_parallel.dart'; export 'src/booleans/boolean_point_in_polygon.dart'; export 'src/booleans/boolean_point_on_line.dart'; diff --git a/lib/line_overlap.dart b/lib/line_overlap.dart new file mode 100644 index 00000000..ba4c0935 --- /dev/null +++ b/lib/line_overlap.dart @@ -0,0 +1,3 @@ +library turf_line_overlap; + +export "src/line_overlap.dart"; diff --git a/lib/src/booleans/boolean_contains.dart b/lib/src/booleans/boolean_contains.dart index 13f77edf..171baaa0 100644 --- a/lib/src/booleans/boolean_contains.dart +++ b/lib/src/booleans/boolean_contains.dart @@ -1,8 +1,5 @@ import 'package:turf/src/invariant.dart'; 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 @@ -32,7 +29,7 @@ bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { if (geom2 is Point) { return coords1 == coords2; } else { - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is MultiPoint) { if (geom2 is Point) { @@ -40,7 +37,7 @@ bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { } else if (geom2 is MultiPoint) { return isMultiPointInMultiPoint(geom2, geom1); } else { - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is LineString) { if (geom2 is Point) { @@ -50,7 +47,7 @@ bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { } else if (geom2 is MultiPoint) { return isMultiPointOnLine(geom2, geom1); } else { - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } else if (geom1 is Polygon) { if (geom2 is Point) { @@ -63,10 +60,10 @@ bool booleanContains(GeoJSONObject feature1, GeoJSONObject feature2) { } else if (geom2 is MultiPoint) { return isMultiPointInPolygon(geom2, geom1); } else { - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } else { - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } diff --git a/lib/src/booleans/boolean_helper.dart b/lib/src/booleans/boolean_helper.dart index 9ecea583..88eaa5b2 100644 --- a/lib/src/booleans/boolean_helper.dart +++ b/lib/src/booleans/boolean_helper.dart @@ -4,15 +4,22 @@ import 'package:turf/src/bbox.dart'; import 'boolean_point_on_line.dart'; import 'boolean_point_in_polygon.dart'; -class FeatureNotSupported implements Exception { +class GeometryNotSupported implements Exception { + final GeometryObject geometry; + GeometryNotSupported(this.geometry); + + @override + String toString() => "geometry not supported ($geometry)."; +} + +class GeometryCombinationNotSupported implements Exception { final GeometryObject geometry1; final GeometryObject geometry2; - FeatureNotSupported(this.geometry1, this.geometry2); + GeometryCombinationNotSupported(this.geometry1, this.geometry2); @override - String toString() => - "feature geometry not supported ($geometry1, $geometry2)."; + String toString() => "geometry not supported ($geometry1, $geometry2)."; } bool isPointInMultiPoint(Point point, MultiPoint multipoint) { diff --git a/lib/src/booleans/boolean_overlap.dart b/lib/src/booleans/boolean_overlap.dart new file mode 100644 index 00000000..c4f4f36a --- /dev/null +++ b/lib/src/booleans/boolean_overlap.dart @@ -0,0 +1,179 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/line_overlap.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/src/invariant.dart'; +import 'package:turf/src/line_intersect.dart'; +import 'package:turf_equality/turf_equality.dart'; +import 'boolean_helper.dart'; + +/// Takes two geometries [firstFeature] and [secondFeature] and checks if they +/// share an common area but are not completely contained by each other. +/// +/// Supported Geometries are `Feature`, `Feature`, +/// `Feature`, `Feature`, `Feature`. +/// Features must be of the same type. LineString/MultiLineString and +/// Polygon/MultiPolygon combinations are supported. If the Geometries are not +/// supported an [GeometryNotSupported] or [GeometryCombinationNotSupported] +/// error is thrown. +/// +/// Returns false if [firstFeature] and [secondFeature] are equal. +/// - MultiPoint: returns Returns true if the two MultiPoints share any point. +/// - LineString: returns true if the two Lines share any line segment. +/// - Polygon: returns true if the two Polygons intersect. +/// +/// Example: +/// ```dart +/// final first = Polygon(coordinates: [ +/// [ +/// Position(0, 0), +/// Position(0, 5), +/// Position(5, 5), +/// Position(5, 0), +/// Position(0, 0) +/// ] +/// ]); +/// final second = Polygon(coordinates: [ +/// [ +/// Position(1, 1), +/// Position(1, 6), +/// Position(6, 6), +/// Position(6, 1), +/// Position(1, 1) +/// ] +/// ]); +/// final third = Polygon(coordinates: [ +/// [ +/// Position(10, 10), +/// Position(10, 15), +/// Position(15, 15), +/// Position(15, 10), +/// Position(10, 10) +/// ] +/// ]); +/// +/// final isOverlapping = booleanOverlap(first, second); +/// final isNotOverlapping = booleanOverlap(second, third); +/// ``` +bool booleanOverlap( + Feature firstFeature, + Feature secondFeature, +) { + final first = getGeom(firstFeature); + final second = getGeom(secondFeature); + + _checkIfGeometryCombinationIsSupported(first, second); + + final eq = Equality( + reversedGeometries: true, + shiftedPolygons: true, + ); + if (eq.compare(first, second)) { + return false; + } + + switch (first.runtimeType) { + case MultiPoint: + switch (second.runtimeType) { + case MultiPoint: + return _isMultiPointOverlapping( + first as MultiPoint, + second as MultiPoint, + ); + default: + throw GeometryCombinationNotSupported(first, second); + } + case MultiLineString: + case LineString: + switch (second.runtimeType) { + case LineString: + case MultiLineString: + return _isLineOverlapping(first, second); + default: + throw GeometryCombinationNotSupported(first, second); + } + case MultiPolygon: + case Polygon: + switch (second.runtimeType) { + case Polygon: + case MultiPolygon: + return _isPolygonOverlapping(first, second); + default: + throw GeometryCombinationNotSupported(first, second); + } + default: + throw GeometryCombinationNotSupported(first, second); + } +} + +bool _isGeometrySupported(GeometryObject geometry) => + geometry is MultiPoint || + geometry is LineString || + geometry is MultiLineString || + geometry is Polygon || + geometry is MultiPolygon; + +void _checkIfGeometryCombinationIsSupported( + GeometryObject first, + GeometryObject second, +) { + if (!_isGeometrySupported(first) || !_isGeometrySupported(second)) { + throw GeometryCombinationNotSupported(first, second); + } +} + +void _checkIfGeometryIsSupported(GeometryObject geometry) { + if (!_isGeometrySupported(geometry)) { + throw GeometryNotSupported(geometry); + } +} + +List> _segmentsOfGeometry(GeometryObject geometry) { + _checkIfGeometryIsSupported(geometry); + List> segments = []; + segmentEach( + geometry, + (Feature segment, _, __, ___, ____) { + segments.add(segment); + }, + ); + return segments; +} + +bool _isLineOverlapping(GeometryObject firstLine, GeometryObject secondLine) { + for (final firstSegment in _segmentsOfGeometry(firstLine)) { + for (final secondSegment in _segmentsOfGeometry(secondLine)) { + if (lineOverlap(firstSegment, secondSegment).features.isNotEmpty) { + return true; + } + } + } + return false; +} + +bool _isPolygonOverlapping( + GeometryObject firstPolygon, + GeometryObject secondPolygon, +) { + for (final firstSegment in _segmentsOfGeometry(firstPolygon)) { + for (final secondSegment in _segmentsOfGeometry(secondPolygon)) { + if (lineIntersect(firstSegment, secondSegment).features.isNotEmpty) { + return true; + } + } + } + return false; +} + +bool _isMultiPointOverlapping( + MultiPoint first, + MultiPoint second, +) { + for (final firstPoint in first.coordinates) { + for (final secondPoint in second.coordinates) { + if (firstPoint == secondPoint) { + return true; + } + } + } + return false; +} diff --git a/lib/src/booleans/boolean_within.dart b/lib/src/booleans/boolean_within.dart index 0b14d8f7..d4697abb 100644 --- a/lib/src/booleans/boolean_within.dart +++ b/lib/src/booleans/boolean_within.dart @@ -20,8 +20,7 @@ import 'boolean_helper.dart'; /// Position.of([1, 4]) /// ], /// ); -/// booleanWithin(point, line); -/// //=true +/// final isWithin = booleanWithin(point, line); // true /// ``` bool booleanWithin( GeoJSONObject feature1, @@ -43,7 +42,7 @@ bool booleanWithin( case MultiPolygon: return isPointInMultiPolygon(point, geom2 as MultiPolygon); default: - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } case MultiPoint: final multipoint = geom1 as MultiPoint; @@ -57,7 +56,7 @@ bool booleanWithin( case MultiPolygon: return isMultiPointInMultiPolygon(multipoint, geom2 as MultiPolygon); default: - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } case LineString: final line = geom1 as LineString; @@ -69,7 +68,7 @@ bool booleanWithin( case MultiPolygon: return isLineInMultiPolygon(line, geom2 as MultiPolygon); default: - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } case Polygon: final polygon = geom1 as Polygon; @@ -79,9 +78,9 @@ bool booleanWithin( case MultiPolygon: return isPolygonInMultiPolygon(polygon, geom2 as MultiPolygon); default: - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } default: - throw FeatureNotSupported(geom1, geom2); + throw GeometryCombinationNotSupported(geom1, geom2); } } diff --git a/lib/src/line_overlap.dart b/lib/src/line_overlap.dart new file mode 100644 index 00000000..51d6ab62 --- /dev/null +++ b/lib/src/line_overlap.dart @@ -0,0 +1,348 @@ +import 'package:rbush/rbush.dart'; +import 'package:turf/line_segment.dart'; +import 'package:turf/meta.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import 'package:turf/turf.dart'; +import 'invariant.dart'; + +/// Takes any [LineString], [MultiLineString], [Polygon] or [MultiPolygon] and +/// returns the overlapping lines between both features. +/// [feature1] first feature +/// [feature2] second feature +/// [tolerance] tolerance distance to match overlapping line segments, default is 0 +/// [unit] the unit in which the tolerance is expressed, default is kilometers +/// returns [FeatureCollection] lines(s) that are overlapping between both features +/// +/// Example +/// ```dart +/// final line1 = lineString([[115, -35], [125, -30], [135, -30], [145, -35]]); +/// final line2 = lineString([[115, -25], [125, -30], [135, -30], [145, -25]]); +/// final overlapping = lineOverlap(line1, line2); +/// ``` +FeatureCollection lineOverlap( + Feature feature1, + Feature feature2, { + num tolerance = 0, + Unit unit = Unit.kilometers, +}) { + if (!_isGeometrySupported(getGeom(feature1)) || + !_isGeometrySupported(getGeom(feature1))) { + throw GeometryCombinationNotSupported( + feature1.geometry!, feature2.geometry!); + } + + final result = >[]; + final tree = _FeatureRBush.create(lineSegment(feature1), tolerance, unit); + + // Iterate over segments of feature1 + segmentEach(feature2, (Feature segmentF2, _, __, ___, ____) { + // detect segments of feature1, that falls within the same + // bonds of the current feature2 segment + featureEach(tree.searchArea(segmentF2), (Feature current, _) { + final segmentF1 = current as Feature; + + // Are the current segments equal? + if (booleanEqual(segmentF2.geometry!, segmentF1.geometry!)) { + // add the complete segment to the result + _addSegmentToResult(result, segmentF2); + // continue with the next feature2 segment + return false; + } + + // Is the segment of feature2 a subset of the feature1 segment? + if (_isSegmentOnLine(segmentF2, segmentF1, tolerance, unit)) { + // add the complete segment to the result + _addSegmentToResult(result, segmentF2); + // continue with the next feature2 segment + return false; + } + + // Is the segment of feature1 a subset of the feature2 segment? + if (_isSegmentOnLine(segmentF1, segmentF2, tolerance, unit)) { + // add only the overlapping part + _addSegmentToResult(result, segmentF1); + // and continue with the next feature1 segment + return true; + } + + // If the segments of feature1 and feature2 didn't share any point and + // the lines are overlapping partially, then we need to create a new + // line segment with the overlapping part and add it to the result. + final overlappingPart = + _getOverlappingPart(segmentF2, segmentF1, tolerance, unit) ?? + _getOverlappingPart(segmentF1, segmentF2, tolerance, unit); + if (overlappingPart != null) { + // add only the overlapping part + _addSegmentToResult(result, overlappingPart); + // and continue with the next feature1 segment + return true; + } + }); + }); + + return FeatureCollection(features: result); +} + +// If both lines didn't share any point, but +// - the start point of the second line is on the first line and +// - the end point of the first line is on the second line and +// - startPoint and endPoint are different, +// we can assume, that both lines are overlapping partially. +// first: .-----------. +// second: .-----------. +// startPoint: . +// endPoint: . +// This solves the issue #901 and #2580 of TurfJs. +Feature? _getOverlappingPart( + Feature first, + Feature second, + num tolerance, + Unit unit, +) { + final firstCoords = _getCoorsSorted(first); + final secondCoords = _getCoorsSorted(second); + final startPoint = Point(coordinates: secondCoords.first); + final endPoint = Point(coordinates: firstCoords.last); + + assert(firstCoords.length == 2, 'only 2 vertex lines are supported'); + assert(secondCoords.length == 2, 'only 2 vertex lines are supported'); + + if (startPoint != endPoint && + _isPointOnLine(startPoint, first, tolerance, unit) && + _isPointOnLine(endPoint, second, tolerance, unit)) { + return Feature( + geometry: LineString(coordinates: [ + startPoint.coordinates, + endPoint.coordinates, + ]), + ); + } + return null; +} + +List _getCoorsSorted(Feature feature) { + final positions = getCoords(feature) as List; + positions.sort((a, b) => a.lng < b.lng + ? -1 + : a.lng > b.lng + ? 1 + : 0); + return positions; +} + +bool _isPointOnLine( + Point point, + Feature line, + num tolerance, + Unit unit, +) { + final lineString = line.geometry as LineString; + + if (tolerance == 0) { + return booleanPointOnLine(point, lineString); + } + final nearestPoint = nearestPointOnLine(lineString, point, unit); + return nearestPoint.properties!['dist'] <= tolerance; +} + +bool _isSegmentOnLine( + Feature segment, + Feature line, + num tolerance, + Unit unit, +) { + final segmentCoords = getCoords(segment) as List; + for (var i = 0; i < segmentCoords.length; i++) { + final point = Point(coordinates: segmentCoords[i]); + if (!_isPointOnLine(point, line, tolerance, unit)) { + return false; + } + } + return true; +} + +void _addSegmentToResult( + List> result, + Feature segment, +) { + // Only add the geometry to the result and remove the feature meta data + final lineSegment = Feature(geometry: segment.geometry); + + // find the feature that can be concatenated with the current segment + for (var i = result.length - 1; i >= 0; i--) { + final combined = _concat(result[i], lineSegment); + if (combined != null) { + result[i] = combined; + return; + } + } + // if no feature was found, add the segment as a new feature + result.add(lineSegment); +} + +Feature? _concat( + Feature line, + Feature segment, +) { + final lineCoords = getCoords(line) as List; + final segmentCoords = getCoords(segment) as List; + assert(lineCoords.length >= 2, 'line must have at least two coordinates.'); + assert(segmentCoords.length == 2, 'segment must have two coordinates.'); + + final lineStart = lineCoords.first; + final lineEnd = lineCoords.last; + final segmentStart = segmentCoords.first; + final segmentEnd = segmentCoords.last; + + List linePositions = + (line.geometry as LineString).clone().coordinates; + + if (segmentStart == lineStart) { + linePositions.insert(0, segmentEnd); + } else if (segmentEnd == lineStart) { + linePositions.insert(0, segmentStart); + } else if (segmentStart == lineEnd) { + linePositions.add(segmentEnd); + } else if (segmentEnd == lineEnd) { + linePositions.add(segmentStart); + } else { + // Segment couldn't be concatenated, because the segment didn't + // share any point with the line. + return null; + } + + return Feature(geometry: LineString(coordinates: linePositions)); +} + +// The RBush Package generally supports own types for the spatial index. +// Something like RBushBase> should be possible, but +// I had problems to get it to work. This is a workaround until I have the +// time to figure out how to use the RBush Package with the Feature +class _FeatureRBush { + _FeatureRBush._( + List>> segments, + this.tolerance, + this.unit, + ) { + _tree = RBushBase>>( + maxEntries: 4, + toBBox: (segment) => _boundingBoxOf(segment), + getMinX: (segment) => _boundingBoxOf(segment).minX, + getMinY: (segment) => _boundingBoxOf(segment).minY, + ); + _tree.load(segments); + } + + late RBushBase>> _tree; + final num tolerance; + final Unit unit; + + static _FeatureRBush create( + FeatureCollection segments, + num tolerance, + Unit unit, + ) { + final converted = segments.features + .map((e) => (e.geometry as LineString) + .coordinates + .map((e) => [e.lng.toDouble(), e.lat.toDouble()]) + .toList()) + .toList(); + + return _FeatureRBush._(converted, tolerance, unit); + } + + FeatureCollection searchArea(Feature segment) { + final coordinates = segment.geometry!.coordinates + .map((e) => [e.lng.toDouble(), e.lat.toDouble()]) + .toList(); + return _buildFeatureCollection( + _tree.search( + _boundingBoxOf(coordinates), + ), + ); + } + + FeatureCollection _buildFeatureCollection( + List>> result, + ) { + return FeatureCollection( + features: result + .map( + (e) => Feature( + geometry: LineString( + coordinates: e.map((e) => Position.of(e)).toList(), + ), + ), + ) + .toList(), + ); + } + + RBushBox _withTolerance(RBushBox box) { + final min = destination( + destination( + Point( + coordinates: Position.named( + lat: box.minX, + lng: box.minY, + ), + ), + tolerance, + 180, + unit, + ), + tolerance, + 270, + unit, + ); + + final max = destination( + destination( + Point( + coordinates: Position.named( + lat: box.maxX, + lng: box.maxY, + ), + ), + tolerance, + 0, + unit), + tolerance, + 90, + unit, + ); + + return RBushBox( + minX: min.coordinates.lat.toDouble(), + minY: min.coordinates.lng.toDouble(), + maxX: max.coordinates.lat.toDouble(), + maxY: max.coordinates.lng.toDouble(), + ); + } + + RBushBox _boundingBoxOf(List> coordinates) { + final box = RBushBox(); + + for (List coordinate in coordinates) { + box.extend(RBushBox( + minX: coordinate[1], // lat1 + minY: coordinate[0], // lng1 + maxX: coordinate[1], // lat2 + maxY: coordinate[0], // lng2 + )); + } + + return tolerance == 0 ? box : _withTolerance(box); + } +} + +bool _isGeometrySupported(GeometryObject geometry) { + if (geometry is LineString || + geometry is MultiLineString || + geometry is Polygon || + geometry is MultiPolygon) { + return true; + } + return false; +} diff --git a/lib/src/line_segment.dart b/lib/src/line_segment.dart index ba7dd164..02df7ad2 100644 --- a/lib/src/line_segment.dart +++ b/lib/src/line_segment.dart @@ -92,7 +92,7 @@ void segmentEach( } if (geometry != null && combineNestedGeometries) { - segmentIndex = _segmentEachforEachUnit( + segmentIndex = _segmentEachForEachUnit( geometry, callback, currentFeature.properties, @@ -112,7 +112,7 @@ void segmentEach( for (int i = 0; i < coords.length; i++) { var line = LineString(coordinates: coords[i]); - segmentIndex = _segmentEachforEachUnit( + segmentIndex = _segmentEachForEachUnit( line, callback, currentFeature.properties, @@ -126,7 +126,7 @@ void segmentEach( ); } -int _segmentEachforEachUnit( +int _segmentEachForEachUnit( GeometryType geometry, SegmentEachCallback callback, Map? currentProperties, diff --git a/lib/src/line_to_polygon.dart b/lib/src/line_to_polygon.dart index 59e01527..eed0aa7c 100644 --- a/lib/src/line_to_polygon.dart +++ b/lib/src/line_to_polygon.dart @@ -62,7 +62,7 @@ Feature lineToPolygon( .map((e) => e.map((p) => p.clone()).toList()) ]; } else { - throw Exception("$currentGeometry type is not supperted"); + throw Exception("$currentGeometry type is not supported"); } }, ); diff --git a/lib/turf.dart b/lib/turf.dart index 25d03aea..cee47a0e 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -15,3 +15,5 @@ export 'src/midpoint.dart'; export 'src/nearest_point.dart'; export 'src/polyline.dart'; export 'src/nearest_point_on_line.dart'; +export 'boolean.dart'; +export 'src/line_overlap.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 26a6587b..5099bcdc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,10 +8,9 @@ repository: https://github.com/dartclub/turf_dart dependencies: json_annotation: ^4.8.1 - - turf_equality: ^0.0.3 + turf_equality: ^0.1.0 turf_pip: ^0.0.2 - rbush: ^1.1.0 + rbush: ^1.1.1 sweepline_intersections: ^0.0.4 dev_dependencies: diff --git a/test/booleans/overlap_test.dart b/test/booleans/overlap_test.dart new file mode 100644 index 00000000..0a309e1e --- /dev/null +++ b/test/booleans/overlap_test.dart @@ -0,0 +1,205 @@ +import 'package:test/test.dart'; +import 'package:turf/src/booleans/boolean_helper.dart'; +import 'package:turf/src/booleans/boolean_overlap.dart'; +import 'package:turf/src/geojson.dart'; + +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; + +void main() { + group('booleanOverlap', () { + final pt = point([9, 50]); + final multiPoint1 = multiPoint([ + [9, 50], + [10, 50], + ]); + final multiPoint2 = multiPoint([ + [9, 50], + [10, 100], + ]); + final line1 = lineString([ + [7, 50], + [8, 50], + [9, 50], + ]); + final line2 = lineString([ + [8, 50], + [9, 50], + [10, 50], + ]); + final poly1 = polygon([ + [ + [8.5, 50], + [9.5, 50], + [9.5, 49], + [8.5, 49], + [8.5, 50], + ], + ]); + final poly2 = polygon([ + [ + [8, 50], + [9, 50], + [9, 49], + [8, 49], + [8, 50], + ], + ]); + final poly3 = polygon([ + [ + [10, 50], + [10.5, 50], + [10.5, 49], + [10, 49], + [10, 50], + ], + ]); + final multiline1 = multiLineString([ + [ + [7, 50], + [8, 50], + [9, 50], + ], + ]); + final multipoly1 = multiPolygon([ + [ + [ + [8.5, 50], + [9.5, 50], + [9.5, 49], + [8.5, 49], + [8.5, 50], + ], + ], + ]); + + test('supported geometries', () { + // points + expect( + () => booleanOverlap(pt, pt), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multiPoint1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, line1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multiline1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(pt, multipoly1), + throwsA(isA()), + ); + + // multiPoints + expect( + () => booleanOverlap(multiPoint1, multiPoint1), + returnsNormally, + ); + expect( + () => booleanOverlap(multiPoint1, line1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, multiline1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiPoint1, multipoly1), + throwsA(isA()), + ); + + // lines + expect( + () => booleanOverlap(line1, line1), + returnsNormally, + ); + expect( + () => booleanOverlap(line1, multiline1), + returnsNormally, + ); + expect( + () => booleanOverlap(line1, poly1), + throwsA(isA()), + ); + + expect( + () => booleanOverlap(line1, multipoly1), + throwsA(isA()), + ); + + // multiline + expect( + () => booleanOverlap(multiline1, multiline1), + returnsNormally, + ); + expect( + () => booleanOverlap(multiline1, poly1), + throwsA(isA()), + ); + expect( + () => booleanOverlap(multiline1, multipoly1), + throwsA(isA()), + ); + + // polygons + expect( + () => booleanOverlap(poly1, poly1), + returnsNormally, + ); + expect( + () => booleanOverlap(poly1, multipoly1), + returnsNormally, + ); + + // multiPolygons + expect( + () => booleanOverlap(multipoly1, multipoly1), + returnsNormally, + ); + }); + + test('equal geometries return false', () { + expect(booleanOverlap(multiPoint1, multiPoint1), false); + expect(booleanOverlap(line1, line1), false); + expect(booleanOverlap(multiline1, multiline1), false); + + expect(booleanOverlap(poly1, poly1), false); + expect(booleanOverlap(multipoly1, multipoly1), false); + }); + + test('overlapping geometries', () { + expect(booleanOverlap(multiPoint1, multiPoint2), true); + expect(booleanOverlap(line1, line2), true); + expect(booleanOverlap(poly1, poly2), true); + expect(booleanOverlap(poly1, poly3), false); + }); + }); + + group('booleanOverlap - examples', () { + loadBooleanTestCases('test/examples/booleans/overlap', ( + path, + geoJsonGiven, + expected, + ) { + final first = (geoJsonGiven as FeatureCollection).features[0]; + final second = geoJsonGiven.features[1]; + test(path, () { + expect(booleanOverlap(first, second), expected, reason: path); + }); + }); + }); +} diff --git a/test/booleans/within_test.dart b/test/booleans/within_test.dart index a37101b3..5bd95821 100644 --- a/test/booleans/within_test.dart +++ b/test/booleans/within_test.dart @@ -1,10 +1,9 @@ -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'; +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; void main() { group('within - true', () { @@ -34,7 +33,7 @@ void main() { 'FeatureNotSupported', () => expect( () => booleanWithin(feature1, feature2), - throwsA(isA()), + throwsA(isA()), ), ); }); @@ -97,46 +96,3 @@ void main() { }); }); } - -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); -} diff --git a/test/components/line_overlap_test.dart b/test/components/line_overlap_test.dart new file mode 100644 index 00000000..41911abf --- /dev/null +++ b/test/components/line_overlap_test.dart @@ -0,0 +1,204 @@ +import 'package:turf/line_overlap.dart'; +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; + +import '../context/helper.dart'; +import '../context/load_test_cases.dart'; +import '../context/matcher.dart' as geo; + +void main() { + group('lineOverlap', () { + final first = lineString([ + [100, -30], + [150, -30], + ]); + test('inner part', () { + final second = lineString([ + [110, -30], + [120, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('start part', () { + final second = lineString([ + [100, -30], + [110, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('two inner segments', () { + final second = lineString([ + [110, -30], + [120, -30], + [130, -30], + ]); + final expected = featureCollection([second]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('multiple segments on the same line', () { + final first = lineString([ + [0, 1], + [1, 1], + [1, 0], + [2, 0], + [2, 1], + [3, 1], + [3, 0], + [4, 0], + [4, 1], + [4, 0], + ]); + final second = lineString([ + [0, 0], + [6, 0], + ]); + + final expected = [ + lineString([ + [1, 0], + [2, 0] + ]), + lineString([ + [3, 0], + [4, 0] + ]), + ]; + + expect(lineOverlap(first, second), geo.contains(expected)); + expect(lineOverlap(second, first), geo.contains(expected)); + }); + test('partial overlap', () { + // bug: https://github.com/Turfjs/turf/issues/2580 + final second = lineString([ + [90, -30], + [110, -30], + ]); + + final expected = featureCollection([ + lineString([ + [100, -30], + [110, -30], + ]) + ]); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('two separate inner segments', () { + final second = lineString([ + [140, -30], + [150, -30], + [150, -20], + [100, -20], + [100, -30], + [110, -30], + ]); + + final expected = featureCollection( + [ + lineString([ + [140, -30], + [150, -30] + ]), + lineString([ + [100, -30], + [110, -30] + ]), + ], + ); + + expect(lineOverlap(first, second), geo.equals(expected)); + expect(lineOverlap(second, first), geo.equals(expected)); + }); + test('validate tolerance', () { + // bug: https://github.com/Turfjs/turf/issues/2582 + // distance between the lines are 11.x km + final first = lineString([ + [10.0, 0.1], + [11.0, 0.1] + ]); + final second = lineString([ + [10.0, 0.0], + [11.0, 0.0] + ]); + + final expected = featureCollection([second]); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + ), + geo.equals(expected), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + unit: Unit.kilometers, + ), + geo.equals(expected), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 11.0, + unit: Unit.kilometers, + ), + geo.length(0), + ); + + expect( + lineOverlap( + first, + second, + tolerance: 12.0, + unit: Unit.meters, + ), + geo.length(0), + ); + }); + }); + + group('lineOverlap - examples', () { + loadTestCases("test/examples/line_overlap", ( + path, + geoJsonGiven, + geoJsonExpected, + ) { + final first = (geoJsonGiven as FeatureCollection).features[0]; + final second = geoJsonGiven.features[1]; + final expectedCollection = geoJsonExpected as FeatureCollection; + + // The last 2 features are equal to the given input. If there are only 2 + // features in the collection it means, that we expect an empty result. + // Otherwise the remaining features are expected. + final expected = expectedCollection.features.length == 2 + ? featureCollection() + : featureCollection( + expectedCollection.features + .sublist(0, expectedCollection.features.length - 2) + .map((e) => Feature(geometry: e.geometry as LineString)) + .toList(), + ); + test(path, () { + final tolerance = first.properties?['tolerance'] ?? 0.0; + final result = lineOverlap(first, second, tolerance: tolerance); + expect(result, geo.equals(expected)); + }); + }); + }); +} diff --git a/test/context/helper.dart b/test/context/helper.dart new file mode 100644 index 00000000..c4782b76 --- /dev/null +++ b/test/context/helper.dart @@ -0,0 +1,70 @@ +// A List of builders that are similar to the way TurfJs creates GeoJSON +// objects. The idea is to make it easier to port JavaScript tests to Dart. +import 'package:turf/turf.dart'; + +Feature point(List coordinates) { + return Feature( + geometry: Point( + coordinates: Position.of(coordinates), + ), + ); +} + +Feature multiPoint(List> coordinates) { + return Feature( + geometry: MultiPoint( + coordinates: + coordinates.map((e) => Position.of(e)).toList(growable: false), + ), + ); +} + +Position position(List coordinates) { + return Position.of(coordinates); +} + +List positions(List> coordinates) { + return coordinates.map((e) => position(e)).toList(growable: false); +} + +Feature lineString(List> coordinates, {dynamic id}) { + return Feature( + id: id, + geometry: LineString(coordinates: positions(coordinates)), + ); +} + +Feature multiLineString( + List>?> coordinates) { + return Feature( + geometry: MultiLineString( + coordinates: coordinates + .map((element) => positions(element!)) + .toList(growable: false)), + ); +} + +Feature polygon(List>> coordinates) { + return Feature( + geometry: Polygon( + coordinates: coordinates + .map((element) => positions(element)) + .toList(growable: false)), + ); +} + +Feature multiPolygon( + List>>?> coordinates) { + return Feature( + geometry: MultiPolygon( + coordinates: coordinates + .map((element) => + element!.map((e) => positions(e)).toList(growable: false)) + .toList(growable: false)), + ); +} + +FeatureCollection featureCollection( + [List> features = const []]) { + return FeatureCollection(features: features); +} diff --git a/test/context/load_test_cases.dart b/test/context/load_test_cases.dart new file mode 100644 index 00000000..7620926b --- /dev/null +++ b/test/context/load_test_cases.dart @@ -0,0 +1,101 @@ +// ignore_for_file: use_rethrow_when_possible + +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; + +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, +) { + var testDirectory = Directory(path); + + for (var file in testDirectory.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + if (file.path.contains('skip')) continue; + + loadGeoJson(file.path, test); + } + } +} + +void loadBooleanTestCases( + String basePath, + void Function( + String path, + GeoJSONObject geoJson, + bool expected, + ) callback, +) { + try { + loadGeoJsonFiles("$basePath/true", (path, geoJson) { + callback(path, geoJson, true); + }); + + loadGeoJsonFiles("$basePath/false", (path, geoJson) { + callback(path, geoJson, false); + }); + } catch (e) { + test('loadBooleanTestCases', () { + expect(() { + throw e; + }, returnsNormally); + }); + } +} + +void loadTestCases( + String basePath, + void Function( + String path, + GeoJSONObject geoJsonGiven, + GeoJSONObject geoJsonExpected, + ) test, +) { + var inDirectory = Directory("$basePath/in"); + var outDirectory = Directory("$basePath/out"); + + if (!inDirectory.existsSync()) { + throw Exception("directory ${inDirectory.path} not found"); + } + if (!outDirectory.existsSync()) { + throw Exception("directory ${outDirectory.path} not found"); + } + + final inFiles = inDirectory + .listSync(recursive: true) + .whereType() + .where( + (file) => + file.path.endsWith('.geojson') && + file.path.contains('skip') == false, + ) + .toList(); + + for (var file in inFiles) { + final outFile = File(file.path.replaceFirst('/in/', '/out/')); + if (outFile.existsSync() == false) { + throw Exception("file ${outFile.path} not found"); + } + + final geoJsonGiven = GeoJSONObject.fromJson( + jsonDecode(file.readAsStringSync()), + ); + + final geoJsonExpected = GeoJSONObject.fromJson( + jsonDecode(outFile.readAsStringSync()), + ); + + test(file.path, geoJsonGiven, geoJsonExpected); + } +} diff --git a/test/context/matcher.dart b/test/context/matcher.dart new file mode 100644 index 00000000..011b03fb --- /dev/null +++ b/test/context/matcher.dart @@ -0,0 +1,87 @@ +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/geojson.dart'; +import 'package:turf_equality/turf_equality.dart'; + +Matcher equals(T? expected) => _Equals(expected); + +class _Equals extends Matcher { + _Equals(this.expected); + final T? expected; + + @override + Description describe(Description description) { + return description.add('is equal'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is! GeoJSONObject) return false; + + Equality eq = Equality(); + return eq.compare(actual, expected); + } +} + +Matcher contains(List expected) => _Contains(expected); + +class _Contains extends Matcher { + _Contains(this.expected); + final List expected; + + @override + Description describe(Description description) { + return description.add('contains'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is! FeatureCollection) throw UnimplementedError(); + + Equality eq = Equality(); + + for (var feature in expected) { + if (!actual.features.any((f) => eq.compare(f, feature))) { + return false; + } + } + return true; + } +} + +Matcher length(int length) => _Length(length); + +class _Length extends Matcher { + _Length(this.length); + final int length; + + @override + Description describe(Description description) { + return description.add('length is $length'); + } + + @override + bool matches(actual, Map matchState) { + if (actual is FeatureCollection) { + return actual.features.length == length; + } + + if (actual is GeometryCollection) { + return actual.geometries.length == length; + } + + if (actual is MultiPoint) { + return actual.coordinates.length == length; + } + + if (actual is MultiPolygon) { + return actual.coordinates.length == length; + } + + if (actual is MultiLineString) { + return actual.coordinates.length == length; + } + + return false; + } +} diff --git a/test/examples/booleans/equal/test/true/lines-reverse.geojson b/test/examples/booleans/equal/test/true/lines-reverse.geojson index 9aa67775..ee136435 100644 --- a/test/examples/booleans/equal/test/true/lines-reverse.geojson +++ b/test/examples/booleans/equal/test/true/lines-reverse.geojson @@ -1,5 +1,8 @@ { "type": "FeatureCollection", + "properties": { + "direction": true + }, "features": [ { "type": "Feature", diff --git a/test/examples/booleans/equal/test/true/reverse-lines.geojson b/test/examples/booleans/equal/test/true/reverse-lines.geojson index 4c6e1810..681f481c 100644 --- a/test/examples/booleans/equal/test/true/reverse-lines.geojson +++ b/test/examples/booleans/equal/test/true/reverse-lines.geojson @@ -1,5 +1,8 @@ { "type": "FeatureCollection", + "properties": { + "direction": true + }, "features": [ { "type": "Feature", diff --git a/test/examples/line_overlap/in/issue-#901.geojson b/test/examples/line_overlap/in/issue-#901.geojson index b897d69b..bbab9617 100644 --- a/test/examples/line_overlap/in/issue-#901.geojson +++ b/test/examples/line_overlap/in/issue-#901.geojson @@ -1,12 +1,10 @@ { "type": "FeatureCollection", - "properties": { - "tolerance": 0.05 - }, "features": [ { "type": "Feature", "properties": { + "tolerance": 0.005, "stroke": "#F00", "fill": "#F00", "stroke-width": 10, diff --git a/test/examples/line_overlap/in/partial-overlap.geojson b/test/examples/line_overlap/in/partial-overlap.geojson new file mode 100644 index 00000000..de005cb3 --- /dev/null +++ b/test/examples/line_overlap/in/partial-overlap.geojson @@ -0,0 +1,46 @@ + +{ + "type": "FeatureCollection", + "features": [ { + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 10, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [130, -35], + [125, -35], + [125, -30], + [135, -30], + [135, -35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "stroke-width": 3, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127, -35], + [125, -35], + [123, -35], + [123, -30], + [125, -30], + [130, -30], + [135, -30], + [135, -33], + [135, -34], + [140, -34] + ] + } + } + ] +} diff --git a/test/examples/line_overlap/in/partial-overlap2.geojson b/test/examples/line_overlap/in/partial-overlap2.geojson new file mode 100644 index 00000000..125866fb --- /dev/null +++ b/test/examples/line_overlap/in/partial-overlap2.geojson @@ -0,0 +1,91 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "fill": "#F00", + "stroke-width": 10, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 110, + -25 + ], + [ + 120, + -25 + ], + [ + 120, + -20 + ], + [ + 125, + -20 + ], + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ], + [ + 135, + -20 + ], + [ + 140, + -20 + ], + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 155, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "fill": "#00F", + "stroke-width": 3, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 115, + -25 + ], + [ + 150, + -25 + ] + ] + } + } + ] + } \ No newline at end of file diff --git a/test/examples/line_overlap/out/issue-#901.geojson b/test/examples/line_overlap/out/issue-#901.geojson index 09d13e5e..b6828142 100644 --- a/test/examples/line_overlap/out/issue-#901.geojson +++ b/test/examples/line_overlap/out/issue-#901.geojson @@ -11,67 +11,28 @@ "geometry": { "type": "LineString", "coordinates": [ - [-113.33698987543352, 53.53214475018778], - [-113.33690471442213, 53.53212132654082], - [-113.33698987543352, 53.53214475018778], - [-113.33704111881613, 53.53215959791441] - ] - }, - "bbox": [ - -113.33704111881613, - 53.53214475018778, - -113.33698987543352, - 53.53215959791441 - ], - "id": 13 - }, - { - "type": "Feature", - "properties": { - "stroke": "#0F0", - "fill": "#0F0", - "stroke-width": 25 - }, - "geometry": { - "type": "LineString", - "coordinates": [ - [-113.33698987543352, 53.53214475018778], - [-113.33690471442213, 53.53212132654082], - [-113.33698987543352, 53.53214475018778], - [-113.33704111881613, 53.53215959791441] - ] - }, - "bbox": [ - -113.33704111881613, - 53.53214475018778, - -113.33698987543352, - 53.53215959791441 - ], - "id": 13 - }, - { - "type": "Feature", - "properties": { - "stroke": "#0F0", - "fill": "#0F0", - "stroke-width": 25 - }, - "geometry": { - "type": "LineString", - "coordinates": [ - [-113.33832502043951, 53.52244398828247], - [-113.3384152645109, 53.52244409344282], - [-113.33847575084239, 53.52244416392682], - [-113.3384152645109, 53.52244409344282] + [ + -113.33690471442213, + 53.53212132654082 + ], + [ + -113.33698987543352, + 53.53214475018778 + ], + [ + -113.33704111881613, + 53.53215959791441 + ], + [ + -113.33698987543352, + 53.53214475018778 + ], + [ + -113.33704111881613, + 53.53215959791441 + ] ] - }, - "bbox": [ - -113.3384152645109, - 53.52244398828247, - -113.33832502043951, - 53.52244409344282 - ], - "id": 20 + } }, { "type": "Feature", @@ -83,20 +44,30 @@ "geometry": { "type": "LineString", "coordinates": [ - [-113.33832502043951, 53.52244398828247], - [-113.3384152645109, 53.52244409344282], - [-113.33847575084239, 53.52244416392682], - [-113.3384152645109, 53.52244409344282] + [ + -113.33832502043951, + 53.52244398828247 + ], + [ + -113.3384152645109, + 53.52244409344282 + ], + [ + -113.33847575084239, + 53.52244416392682 + ], + [ + -113.3384152645109, + 53.52244409344282 + ], + [ + -113.33847575084239, + 53.52244416392682 + ] ] - }, - "bbox": [ - -113.3384152645109, - 53.52244398828247, - -113.33832502043951, - 53.52244409344282 - ], - "id": 20 + } }, + { "type": "Feature", "properties": { diff --git a/test/examples/line_overlap/out/partial-overlap.geojson b/test/examples/line_overlap/out/partial-overlap.geojson new file mode 100644 index 00000000..dc73c96f --- /dev/null +++ b/test/examples/line_overlap/out/partial-overlap.geojson @@ -0,0 +1,79 @@ + +{ + "type": "FeatureCollection", + "features": [ { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127,-35], + [125,-35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [125,-30], + [130,-30], + [135,-30], + [135,-33], + [135,-34] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 10, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [130, -35], + [125, -35], + [125, -30], + [135, -30], + [135, -35] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "stroke-width": 3, + "stroke-opacity": 1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [127, -35], + [125, -35], + [123, -35], + [123, -30], + [125, -30], + [130, -30], + [135, -30], + [135, -33], + [135, -34], + [140, -34] + ] + } + } + ] +} diff --git a/test/examples/line_overlap/out/partial-overlap2.geojson b/test/examples/line_overlap/out/partial-overlap2.geojson new file mode 100644 index 00000000..9a9456dd --- /dev/null +++ b/test/examples/line_overlap/out/partial-overlap2.geojson @@ -0,0 +1,166 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 150, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#0F0", + "fill": "#0F0", + "stroke-width": 25 + }, + "geometry": { + "type": "LineString", + "bbox": null, + "coordinates": [ + [ + 115, + -25 + ], + [ + 120, + -25 + ] + ] + } + }, + + { + "type": "Feature", + "properties": { + "stroke": "#F00", + "fill": "#F00", + "stroke-width": 10, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 110, + -25 + ], + [ + 120, + -25 + ], + [ + 120, + -20 + ], + [ + 125, + -20 + ], + [ + 125, + -25 + ], + [ + 130, + -25 + ], + [ + 135, + -25 + ], + [ + 135, + -20 + ], + [ + 140, + -20 + ], + [ + 140, + -25 + ], + [ + 145, + -25 + ], + [ + 155, + -25 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "stroke": "#00F", + "fill": "#00F", + "stroke-width": 3, + "stroke-opacity": 1, + "fill-opacity": 0.1 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 115, + -25 + ], + [ + 150, + -25 + ] + ] + } + } + ] + } \ No newline at end of file