diff --git a/README.md b/README.md index d6e131f..277068b 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Any new benchmarks must be named `*_benchmark.dart` and reside in the - [ ] [lineIntersect](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_intersect.dart) - [ ] lineOverlap - [x] [lineSegment](https://github.com/dartclub/turf_dart/blob/main/lib/src/line_segment.dart) -- [ ] lineSlice +- [x] lineSlice - [ ] lineSliceAlong - [ ] lineSplit - [ ] mask diff --git a/lib/line_slice.dart b/lib/line_slice.dart new file mode 100644 index 0000000..dafdd38 --- /dev/null +++ b/lib/line_slice.dart @@ -0,0 +1,3 @@ +library turf_along; + +export "src/line_slice.dart"; diff --git a/lib/src/line_slice.dart b/lib/src/line_slice.dart new file mode 100644 index 0000000..cbd53b2 --- /dev/null +++ b/lib/src/line_slice.dart @@ -0,0 +1,47 @@ +import 'package:turf/helpers.dart'; +import 'package:turf/nearest_point_on_line.dart'; +import 'package:turf/src/invariant.dart'; + +/// Takes a [line], at a start point [startPt], and a stop point [stopPt] +/// and returns a subsection of the line in-between those points. +/// The start & stop points don't need to fall exactly on the line. +/// +/// If [startPt] and [stopPt] resolve to the same point on [line], null is returned +/// as the resolved line would only contain one point which isn't supported by LineString. +/// +/// This can be useful for extracting only the part of a route between waypoints. +Feature lineSlice( + Feature startPt, Feature stopPt, Feature line) { + final coords = line.geometry; + final startPtGeometry = startPt.geometry; + final stopPtGeometry = stopPt.geometry; + if (coords == null) { + throw Exception('line has no geometry'); + } + if (startPtGeometry == null) { + throw Exception('startPt has no geometry'); + } + if (stopPtGeometry == null) { + throw Exception('stopPt has no geometry'); + } + + final startVertex = nearestPointOnLine(coords, startPtGeometry); + final stopVertex = nearestPointOnLine(coords, stopPtGeometry); + late final List> ends; + if (startVertex.properties!['index'] <= stopVertex.properties!['index']) { + ends = [startVertex, stopVertex]; + } else { + ends = [stopVertex, startVertex]; + } + final List clipCoords = [getCoord(ends[0])]; + for (var i = ends[0].properties!['index'] + 1; + i < ends[1].properties!['index']; + i++) { + clipCoords.add(coords.coordinates[i]); + } + clipCoords.add(getCoord(ends[1])); + return Feature( + geometry: LineString(coordinates: clipCoords), + properties: line.properties, + ); +} diff --git a/test/components/line_slice_test.dart b/test/components/line_slice_test.dart new file mode 100644 index 0000000..721e64e --- /dev/null +++ b/test/components/line_slice_test.dart @@ -0,0 +1,78 @@ +import 'package:test/test.dart'; +import 'package:turf/along.dart'; +import 'package:turf/distance.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/length.dart'; +import 'package:turf/src/line_slice.dart'; + +void main() { + test('lineSlice - exact points', () { + final slice = lineSlice(startFeature, viaFeature, lineFeature); + expect(slice, isNotNull); + expect(slice.properties, isNotNull); + expect(slice.properties!.keys, contains(propName)); + expect(slice.properties![propName], equals(propValue)); + + final expectedLineFeature = Feature( + geometry: LineString(coordinates: [start, via]), + ); + expect(slice.geometry, isNotNull); + expect(slice.geometry!.coordinates, hasLength(2)); + expect(length(slice).round(), equals(length(expectedLineFeature).round())); + }); + test('lineSlice - interpolation', () { + const skipDist = 10; + + final sliceFrom = along(lineFeature, skipDist, Unit.meters); + expect(sliceFrom, isNotNull); + + final slice = lineSlice(sliceFrom, viaFeature, lineFeature); + expect(slice, isNotNull); + expect(slice.properties, isNotNull); + expect(slice.properties!.keys, contains(propName)); + expect(slice.properties![propName], equals(propValue)); + + final expectedLine = Feature( + geometry: LineString(coordinates: [start, via]), + ); + expect(slice.geometry, isNotNull); + expect(slice.geometry!.coordinates, hasLength(2)); + expect( + length(slice, Unit.meters).round(), + equals(length(expectedLine, Unit.meters).round() - skipDist), + ); + + // Sanity check of test data. No interpolation occurs if start and via are skipDist apart. + expect(distance(Point(coordinates: start), Point(coordinates: via)).round(), + isNot(equals(skipDist))); + }); +} + +final start = Position.named( + lat: 55.7090430186194, + lng: 13.184645393920405, +); +final via = Position.named( + lat: 55.70901279569489, + lng: 13.185546616182755, +); +final end = Position.named( + lat: 55.70764669578079, + lng: 13.187563637197076, +); +const propName = 'prop1'; +const propValue = 1; +final lineFeature = Feature( + geometry: LineString( + coordinates: [ + start, + via, + end, + ], + ), + properties: { + propName: propValue, + }, +); +final startFeature = Feature(geometry: Point(coordinates: start)); +final viaFeature = Feature(geometry: Point(coordinates: via));