diff --git a/Progress.md b/Progress.md index e675056..86ab754 100644 --- a/Progress.md +++ b/Progress.md @@ -20,7 +20,7 @@ Dart. This is an on going project and functions are being added once needed. If - [x] [midpoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/midpoint.dart) - [ ] pointOnFeature - [ ] polygonTangents -- [ ] pointToLineDistance +- [x] [pointToLineDistance](https://github.com/dartclub/turf_dart/blob/main/lib/src/point_to_line_distance.dart) - [x] [rhumbBearing](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_bearing.dart) - [x] [rhumbDestination](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_destination.dart) - [x] [rhumbDistance](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_distance.dart) diff --git a/lib/point_to_line_distance.dart b/lib/point_to_line_distance.dart new file mode 100644 index 0000000..2a2a01c --- /dev/null +++ b/lib/point_to_line_distance.dart @@ -0,0 +1,4 @@ +library turf_point_to_line_distance; + +export 'package:geotypes/geotypes.dart'; +export 'src/point_to_line_distance.dart'; diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart index 0e15b96..f82c9b2 100644 --- a/lib/src/helpers.dart +++ b/lib/src/helpers.dart @@ -31,6 +31,18 @@ enum Corner { centroid, } +/// Whether to calculate the distance based on geodesic (spheroid) or +/// planar (flat) method. +enum DistanceGeometry { + /// Calculations will be made on a 2D plane, NOT taking into account the + /// earth curvature. + planar, + + /// Calculate the distance with geodesic (spheroid) equations. It will take + /// into account the earth as a sphere. + geodesic, +} + /// Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth. const earthRadius = 6371008.8; diff --git a/lib/src/point_to_line_distance.dart b/lib/src/point_to_line_distance.dart new file mode 100644 index 0000000..018bfbe --- /dev/null +++ b/lib/src/point_to_line_distance.dart @@ -0,0 +1,79 @@ +import 'package:turf/distance.dart'; +import 'package:turf/line_segment.dart'; +import 'helpers.dart'; + +// Sourced from https://turfjs.org (MIT license) and from +// http://geomalgorithms.com/a02-_lines.html + +/// Returns the minimum distance between a [point] and a [line], being the +/// distance from a line the minimum distance between the point and any +/// segment of the [LineString]. +/// +/// Example: +/// ```dart +/// final point = Point(coordinates: Position(0, 0)); +/// final line = LineString(coordinates: [Position(1, 1), Position(-1, 1)]); +/// +/// final distance = pointToLineDistance(point, line, unit: Unit.miles); +/// // distance == 69.11854715938406 +/// ``` +num pointToLineDistance( + Point point, + LineString line, { + Unit unit = Unit.kilometers, + DistanceGeometry method = DistanceGeometry.geodesic, +}) { + var distance = double.infinity; + final position = point.coordinates; + + segmentEach(line, (segment, _, __, ___, ____) { + final a = segment.geometry!.coordinates[0]; + final b = segment.geometry!.coordinates[1]; + final d = _distanceToSegment(position, a, b, method: method); + + if (d < distance) { + distance = d.toDouble(); + } + }); + + return convertLength(distance, Unit.degrees, unit); +} + +/// Returns the distance between a point P on a segment AB. +num _distanceToSegment( + Position p, + Position a, + Position b, { + required DistanceGeometry method, +}) { + final v = b - a; + final w = p - a; + + final c1 = w.dotProduct(v); + if (c1 <= 0) { + return _calcDistance(p, a, method: method, unit: Unit.degrees); + } + + final c2 = v.dotProduct(v); + if (c2 <= c1) { + return _calcDistance(p, b, method: method, unit: Unit.degrees); + } + + final b2 = c1 / c2; + final pb = a + Position(v[0]! * b2, v[1]! * b2); + return _calcDistance(p, pb, method: method, unit: Unit.degrees); +} + +num _calcDistance( + Position a, + Position b, { + required Unit unit, + required DistanceGeometry method, +}) { + if (method == DistanceGeometry.planar) { + return rhumbDistance(Point(coordinates: a), Point(coordinates: b), unit); + } + + // Otherwise DistanceGeometry.geodesic + return distanceRaw(a, b, unit); +} diff --git a/lib/turf.dart b/lib/turf.dart index b36424c..df19263 100644 --- a/lib/turf.dart +++ b/lib/turf.dart @@ -27,6 +27,7 @@ export 'meta.dart'; export 'midpoint.dart'; export 'nearest_point_on_line.dart'; export 'nearest_point.dart'; +export 'point_to_line_distance.dart'; export 'polygon_smooth.dart'; export 'polygon_to_line.dart'; export 'polyline.dart'; diff --git a/test/components/point_to_line_distance_test.dart b/test/components/point_to_line_distance_test.dart new file mode 100644 index 0000000..1110401 --- /dev/null +++ b/test/components/point_to_line_distance_test.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:turf/helpers.dart'; +import 'package:turf/src/point_to_line_distance.dart'; + +final distances = { + "city-line1.geojson": 1.0299686758, + "city-line2.geojson": 3.6186172981, + "city-segment-inside1.geojson": 1.1489389115, + "city-segment-inside2.geojson": 1.0280898152, + "city-segment-inside3.geojson": 3.5335695907, + "city-segment-obtuse1.geojson": 2.8573246363, + "city-segment-obtuse2.geojson": 3.3538913334, + "city-segment-projected1.geojson": 3.5886611693, + "city-segment-projected2.geojson": 4.163469898, + "issue-1156.geojson": 189.6618028794, + "line-fiji.geojson": 27.1266612008, + "line-resolute-bay.geojson": 425.0745081528, + "line1.geojson": 23.4224834672, + "line2.geojson": 188.015686924, + "segment-fiji.geojson": 27.6668301762, + "segment1.geojson": 69.0934195756, + "segment1a.geojson": 69.0934195756, + "segment2.geojson": 69.0934195756, + "segment3.geojson": 69.0828960461, + "segment4.geojson": 332.8803863574 +}; + +void main() { + group('pointToLineDistance', () { + group('in == out', () { + final inDir = Directory('./test/examples/point_to_line_distance/in'); + + for (final file in inDir.listSync(recursive: true)) { + if (file is File && file.path.endsWith('.geojson')) { + testFile(file); + } + } + }); + + group('unit tests', () { + testPlanarGeodesic(); + }); + }); +} + +void testFile(File file) { + test(file.path, () { + final inSource = file.readAsStringSync(); + final collection = FeatureCollection.fromJson(jsonDecode(inSource)); + + final rawPoint = collection.features[0]; + final rawLine = collection.features[1]; + + final point = Feature.fromJson(rawPoint.toJson()); + final line = Feature.fromJson(rawLine.toJson()); + + final properties = rawPoint.properties ?? {}; + final unitRaw = properties["units"] as String?; + + var unit = Unit.kilometers; + if (unitRaw == 'meters') { + unit = Unit.meters; + } else if (unitRaw == 'miles') { + unit = Unit.miles; + } else { + expect(unitRaw, null, reason: '"units" was given but not handled.'); + } + + final distance = + pointToLineDistance(point.geometry!, line.geometry!, unit: unit); + + final name = file.path.substring(file.path.lastIndexOf('/') + 1); + + expect(distance, closeTo(distances[name]!, 0.01)); + }); +} + +void testPlanarGeodesic() { + test('Check planar and geodesic results are different', () { + final pt = Point(coordinates: Position(0, 0)); + final line = LineString(coordinates: [ + Position(10, 10), + Position(-1, 1), + ]); + + final geoOut = + pointToLineDistance(pt, line, method: DistanceGeometry.geodesic); + final planarOut = + pointToLineDistance(pt, line, method: DistanceGeometry.planar); + + expect(geoOut, isNot(equals(planarOut))); + }); +} diff --git a/test/examples/point_to_line_distance/in/city-line1.geojson b/test/examples/point_to_line_distance/in/city-line1.geojson new file mode 100644 index 0000000..22bcbdc --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-line1.geojson @@ -0,0 +1,30 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-0.3767967224121093, 39.4689324766527] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-0.40567874908447266, 39.47386857192064], + [-0.3963661193847656, 39.47578991028725], + [-0.38035869598388666, 39.482216070269594], + [-0.3776121139526367, 39.48195108571802], + [-0.3689002990722656, 39.47641930269614], + [-0.35945892333984375, 39.46349905420083], + [-0.35782814025878906, 39.45982131412374], + [-0.3458118438720703, 39.453890134716616] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-line2.geojson b/test/examples/point_to_line_distance/in/city-line2.geojson new file mode 100644 index 0000000..c565805 --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-line2.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-3.592529296875, 40.573804799488194] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-3.8884735107421875, 40.420292132688964], + [-3.736724853515625, 40.276906410822825], + [-3.5025787353515625, 40.422383097039905], + [-3.5018920898437496, 40.516409213865586], + [-3.668060302734375, 40.559199680578075] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-inside1.geojson b/test/examples/point_to_line_distance/in/city-segment-inside1.geojson new file mode 100644 index 0000000..08a0ba7 --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-inside1.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [-6.0047149658203125, 37.365109304227246] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-6.0150146484375, 37.38011551844836], + [-5.931415557861328, 37.39702801486944] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-inside2.geojson b/test/examples/point_to_line_distance/in/city-segment-inside2.geojson new file mode 100644 index 0000000..a8fdfc0 --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-inside2.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-0.3767967224121093, 39.4689324766527] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-0.3689861297607422, 39.47648555419739], + [-0.3595447540283203, 39.46363158174706] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-inside3.geojson b/test/examples/point_to_line_distance/in/city-segment-inside3.geojson new file mode 100644 index 0000000..1676b38 --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-inside3.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-3.592529296875, 40.573804799488194] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-3.503265380859375, 40.51693121343741], + [-3.6694335937500004, 40.560764667193595] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-obtuse1.geojson b/test/examples/point_to_line_distance/in/city-segment-obtuse1.geojson new file mode 100644 index 0000000..3f8b925 --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-obtuse1.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-6.030292510986328, 37.35746862390723] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-6.0150146484375, 37.38011551844836], + [-5.931415557861328, 37.39702801486944] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-obtuse2.geojson b/test/examples/point_to_line_distance/in/city-segment-obtuse2.geojson new file mode 100644 index 0000000..3414bea --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-obtuse2.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-6.025829315185546, 37.40902811697313] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-6.0150146484375, 37.38011551844836], + [-5.932445526123047, 37.36770150115655] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-projected1.geojson b/test/examples/point_to_line_distance/in/city-segment-projected1.geojson new file mode 100644 index 0000000..1a462ce --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-projected1.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-5.914421081542969, 37.426343057829385] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-6.0150146484375, 37.38011551844836], + [-5.931415557861328, 37.39702801486944] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/city-segment-projected2.geojson b/test/examples/point_to_line_distance/in/city-segment-projected2.geojson new file mode 100644 index 0000000..10d6e9e --- /dev/null +++ b/test/examples/point_to_line_distance/in/city-segment-projected2.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-5.926437377929687, 37.32785364060672] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-6.0150146484375, 37.38011551844836], + [-5.9326171875, 37.364972870329154] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/issue-1156.geojson b/test/examples/point_to_line_distance/in/issue-1156.geojson new file mode 100644 index 0000000..295e6b8 --- /dev/null +++ b/test/examples/point_to_line_distance/in/issue-1156.geojson @@ -0,0 +1,28 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "meters" + }, + "geometry": { + "type": "Point", + "coordinates": [11.028348, 41] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [10.983200073242188, 40.97075154073346], + [11.02834701538086, 40.98372150040732], + [11.02508544921875, 41.00716631272605], + [10.994186401367188, 41.01947819666632] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/line-fiji.geojson b/test/examples/point_to_line_distance/in/line-fiji.geojson new file mode 100644 index 0000000..fe22b79 --- /dev/null +++ b/test/examples/point_to_line_distance/in/line-fiji.geojson @@ -0,0 +1,37 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-180.46142578124997, -17.481671724450752] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-183.131103515625, -17.675427818339383], + [-181.50512695312497, -16.04581345375217], + [-177.802734375, -16.615137799987075], + [-179.47265625, -17.947380678685203], + [-179.60449218749997, -16.383391123608387], + [-181.087646484375, -17.70682812401954], + [-179.93408203125, -15.432500881886043], + [-180.010986328125, -18.458768120015126], + [-181.834716796875, -17.832374329567507], + [-180.15380859375, -15.601874876739798], + [-178.08837890625, -16.320139453117562], + [-179.01123046874997, -18.98941471523932], + [-183.240966796875, -16.53089842368168], + [-182.4169921875, -18.67747125852608], + [-182.274169921875, -16.003575733881313] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/line-resolute-bay.geojson b/test/examples/point_to_line_distance/in/line-resolute-bay.geojson new file mode 100644 index 0000000..90a818d --- /dev/null +++ b/test/examples/point_to_line_distance/in/line-resolute-bay.geojson @@ -0,0 +1,27 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [-7.734374999999999, 70.4367988185464] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-60.8203125, 64.99793920061401], + [-31.640625, 76.96033358827414], + [-5.625, 76.55774293896555] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/line1.geojson b/test/examples/point_to_line_distance/in/line1.geojson new file mode 100644 index 0000000..f75590f --- /dev/null +++ b/test/examples/point_to_line_distance/in/line1.geojson @@ -0,0 +1,34 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [-47.08740234375, 32.26855544621476] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-55.06347656249999, 29.973970240516614], + [-45.439453125, 36.58024660149866], + [-49.04296875, 31.914867503276223], + [-43.39599609375, 33.99802726234877], + [-46.51611328125, 29.707139348134145], + [-52.4267578125, 32.37996146435729], + [-50.11962890625, 28.43971381702788], + [-52.998046875, 27.430289738862594], + [-50.0537109375, 35.02999636902566], + [-42.38525390625, 31.466153715024294] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/line2.geojson b/test/examples/point_to_line_distance/in/line2.geojson new file mode 100644 index 0000000..29c6d16 --- /dev/null +++ b/test/examples/point_to_line_distance/in/line2.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-0.54931640625, 0.7470491450051796] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [1, 3], + [2, 2], + [2, 0], + [-1.5, -1.5] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment-fiji.geojson b/test/examples/point_to_line_distance/in/segment-fiji.geojson new file mode 100644 index 0000000..b550a27 --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment-fiji.geojson @@ -0,0 +1,24 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [-180.46142578124997, -17.481671724450752] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-181.087646484375, -17.69897856226166], + [-179.60586547851562, -16.380756046586434] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment1.geojson b/test/examples/point_to_line_distance/in/segment1.geojson new file mode 100644 index 0000000..f780a2d --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment1.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [2, 0] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment1a.geojson b/test/examples/point_to_line_distance/in/segment1a.geojson new file mode 100644 index 0000000..a8cdc8f --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment1a.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [1, -1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [2, 0] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment2.geojson b/test/examples/point_to_line_distance/in/segment2.geojson new file mode 100644 index 0000000..bd80cfd --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment2.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [0, 0] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [1, 1], + [1, -1] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment3.geojson b/test/examples/point_to_line_distance/in/segment3.geojson new file mode 100644 index 0000000..ce94909 --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment3.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [2, 1], + [3, 1] + ] + } + } + ] +} diff --git a/test/examples/point_to_line_distance/in/segment4.geojson b/test/examples/point_to_line_distance/in/segment4.geojson new file mode 100644 index 0000000..50511dc --- /dev/null +++ b/test/examples/point_to_line_distance/in/segment4.geojson @@ -0,0 +1,26 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "units": "miles" + }, + "geometry": { + "type": "Point", + "coordinates": [-48.076171875, 15.453680224345835] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-51.5478515625, 19.352610894378625], + [-29.794921874999996, 27.176469131898898] + ] + } + } + ] +}