Skip to content

Commit da48c29

Browse files
authored
feat: implement pointToLineDistance (#189)
1 parent 0a58cb6 commit da48c29

26 files changed

+726
-1
lines changed

Progress.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Dart. This is an on going project and functions are being added once needed. If
2020
- [x] [midpoint](https://github.com/dartclub/turf_dart/blob/main/lib/src/midpoint.dart)
2121
- [ ] pointOnFeature
2222
- [ ] polygonTangents
23-
- [ ] pointToLineDistance
23+
- [x] [pointToLineDistance](https://github.com/dartclub/turf_dart/blob/main/lib/src/point_to_line_distance.dart)
2424
- [x] [rhumbBearing](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_bearing.dart)
2525
- [x] [rhumbDestination](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_destination.dart)
2626
- [x] [rhumbDistance](https://github.com/dartclub/turf_dart/blob/main/lib/src/rhumb_distance.dart)

lib/point_to_line_distance.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
library turf_point_to_line_distance;
2+
3+
export 'package:geotypes/geotypes.dart';
4+
export 'src/point_to_line_distance.dart';

lib/src/helpers.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ enum Corner {
3131
centroid,
3232
}
3333

34+
/// Whether to calculate the distance based on geodesic (spheroid) or
35+
/// planar (flat) method.
36+
enum DistanceGeometry {
37+
/// Calculations will be made on a 2D plane, NOT taking into account the
38+
/// earth curvature.
39+
planar,
40+
41+
/// Calculate the distance with geodesic (spheroid) equations. It will take
42+
/// into account the earth as a sphere.
43+
geodesic,
44+
}
45+
3446
/// Earth Radius used with the Harvesine formula and approximates using a spherical (non-ellipsoid) Earth.
3547
const earthRadius = 6371008.8;
3648

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:turf/distance.dart';
2+
import 'package:turf/line_segment.dart';
3+
import 'helpers.dart';
4+
5+
// Sourced from https://turfjs.org (MIT license) and from
6+
// http://geomalgorithms.com/a02-_lines.html
7+
8+
/// Returns the minimum distance between a [point] and a [line], being the
9+
/// distance from a line the minimum distance between the point and any
10+
/// segment of the [LineString].
11+
///
12+
/// Example:
13+
/// ```dart
14+
/// final point = Point(coordinates: Position(0, 0));
15+
/// final line = LineString(coordinates: [Position(1, 1), Position(-1, 1)]);
16+
///
17+
/// final distance = pointToLineDistance(point, line, unit: Unit.miles);
18+
/// // distance == 69.11854715938406
19+
/// ```
20+
num pointToLineDistance(
21+
Point point,
22+
LineString line, {
23+
Unit unit = Unit.kilometers,
24+
DistanceGeometry method = DistanceGeometry.geodesic,
25+
}) {
26+
var distance = double.infinity;
27+
final position = point.coordinates;
28+
29+
segmentEach(line, (segment, _, __, ___, ____) {
30+
final a = segment.geometry!.coordinates[0];
31+
final b = segment.geometry!.coordinates[1];
32+
final d = _distanceToSegment(position, a, b, method: method);
33+
34+
if (d < distance) {
35+
distance = d.toDouble();
36+
}
37+
});
38+
39+
return convertLength(distance, Unit.degrees, unit);
40+
}
41+
42+
/// Returns the distance between a point P on a segment AB.
43+
num _distanceToSegment(
44+
Position p,
45+
Position a,
46+
Position b, {
47+
required DistanceGeometry method,
48+
}) {
49+
final v = b - a;
50+
final w = p - a;
51+
52+
final c1 = w.dotProduct(v);
53+
if (c1 <= 0) {
54+
return _calcDistance(p, a, method: method, unit: Unit.degrees);
55+
}
56+
57+
final c2 = v.dotProduct(v);
58+
if (c2 <= c1) {
59+
return _calcDistance(p, b, method: method, unit: Unit.degrees);
60+
}
61+
62+
final b2 = c1 / c2;
63+
final pb = a + Position(v[0]! * b2, v[1]! * b2);
64+
return _calcDistance(p, pb, method: method, unit: Unit.degrees);
65+
}
66+
67+
num _calcDistance(
68+
Position a,
69+
Position b, {
70+
required Unit unit,
71+
required DistanceGeometry method,
72+
}) {
73+
if (method == DistanceGeometry.planar) {
74+
return rhumbDistance(Point(coordinates: a), Point(coordinates: b), unit);
75+
}
76+
77+
// Otherwise DistanceGeometry.geodesic
78+
return distanceRaw(a, b, unit);
79+
}

lib/turf.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export 'meta.dart';
2727
export 'midpoint.dart';
2828
export 'nearest_point_on_line.dart';
2929
export 'nearest_point.dart';
30+
export 'point_to_line_distance.dart';
3031
export 'polygon_smooth.dart';
3132
export 'polygon_to_line.dart';
3233
export 'polyline.dart';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:test/test.dart';
5+
import 'package:turf/helpers.dart';
6+
import 'package:turf/src/point_to_line_distance.dart';
7+
8+
final distances = {
9+
"city-line1.geojson": 1.0299686758,
10+
"city-line2.geojson": 3.6186172981,
11+
"city-segment-inside1.geojson": 1.1489389115,
12+
"city-segment-inside2.geojson": 1.0280898152,
13+
"city-segment-inside3.geojson": 3.5335695907,
14+
"city-segment-obtuse1.geojson": 2.8573246363,
15+
"city-segment-obtuse2.geojson": 3.3538913334,
16+
"city-segment-projected1.geojson": 3.5886611693,
17+
"city-segment-projected2.geojson": 4.163469898,
18+
"issue-1156.geojson": 189.6618028794,
19+
"line-fiji.geojson": 27.1266612008,
20+
"line-resolute-bay.geojson": 425.0745081528,
21+
"line1.geojson": 23.4224834672,
22+
"line2.geojson": 188.015686924,
23+
"segment-fiji.geojson": 27.6668301762,
24+
"segment1.geojson": 69.0934195756,
25+
"segment1a.geojson": 69.0934195756,
26+
"segment2.geojson": 69.0934195756,
27+
"segment3.geojson": 69.0828960461,
28+
"segment4.geojson": 332.8803863574
29+
};
30+
31+
void main() {
32+
group('pointToLineDistance', () {
33+
group('in == out', () {
34+
final inDir = Directory('./test/examples/point_to_line_distance/in');
35+
36+
for (final file in inDir.listSync(recursive: true)) {
37+
if (file is File && file.path.endsWith('.geojson')) {
38+
testFile(file);
39+
}
40+
}
41+
});
42+
43+
group('unit tests', () {
44+
testPlanarGeodesic();
45+
});
46+
});
47+
}
48+
49+
void testFile(File file) {
50+
test(file.path, () {
51+
final inSource = file.readAsStringSync();
52+
final collection = FeatureCollection.fromJson(jsonDecode(inSource));
53+
54+
final rawPoint = collection.features[0];
55+
final rawLine = collection.features[1];
56+
57+
final point = Feature<Point>.fromJson(rawPoint.toJson());
58+
final line = Feature<LineString>.fromJson(rawLine.toJson());
59+
60+
final properties = rawPoint.properties ?? {};
61+
final unitRaw = properties["units"] as String?;
62+
63+
var unit = Unit.kilometers;
64+
if (unitRaw == 'meters') {
65+
unit = Unit.meters;
66+
} else if (unitRaw == 'miles') {
67+
unit = Unit.miles;
68+
} else {
69+
expect(unitRaw, null, reason: '"units" was given but not handled.');
70+
}
71+
72+
final distance =
73+
pointToLineDistance(point.geometry!, line.geometry!, unit: unit);
74+
75+
final name = file.path.substring(file.path.lastIndexOf('/') + 1);
76+
77+
expect(distance, closeTo(distances[name]!, 0.01));
78+
});
79+
}
80+
81+
void testPlanarGeodesic() {
82+
test('Check planar and geodesic results are different', () {
83+
final pt = Point(coordinates: Position(0, 0));
84+
final line = LineString(coordinates: [
85+
Position(10, 10),
86+
Position(-1, 1),
87+
]);
88+
89+
final geoOut =
90+
pointToLineDistance(pt, line, method: DistanceGeometry.geodesic);
91+
final planarOut =
92+
pointToLineDistance(pt, line, method: DistanceGeometry.planar);
93+
94+
expect(geoOut, isNot(equals(planarOut)));
95+
});
96+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "Point",
9+
"coordinates": [-0.3767967224121093, 39.4689324766527]
10+
}
11+
},
12+
{
13+
"type": "Feature",
14+
"properties": {},
15+
"geometry": {
16+
"type": "LineString",
17+
"coordinates": [
18+
[-0.40567874908447266, 39.47386857192064],
19+
[-0.3963661193847656, 39.47578991028725],
20+
[-0.38035869598388666, 39.482216070269594],
21+
[-0.3776121139526367, 39.48195108571802],
22+
[-0.3689002990722656, 39.47641930269614],
23+
[-0.35945892333984375, 39.46349905420083],
24+
[-0.35782814025878906, 39.45982131412374],
25+
[-0.3458118438720703, 39.453890134716616]
26+
]
27+
}
28+
}
29+
]
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "Point",
9+
"coordinates": [-3.592529296875, 40.573804799488194]
10+
}
11+
},
12+
{
13+
"type": "Feature",
14+
"properties": {},
15+
"geometry": {
16+
"type": "LineString",
17+
"coordinates": [
18+
[-3.8884735107421875, 40.420292132688964],
19+
[-3.736724853515625, 40.276906410822825],
20+
[-3.5025787353515625, 40.422383097039905],
21+
[-3.5018920898437496, 40.516409213865586],
22+
[-3.668060302734375, 40.559199680578075]
23+
]
24+
}
25+
}
26+
]
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {
7+
"units": "miles"
8+
},
9+
"geometry": {
10+
"type": "Point",
11+
"coordinates": [-6.0047149658203125, 37.365109304227246]
12+
}
13+
},
14+
{
15+
"type": "Feature",
16+
"properties": {},
17+
"geometry": {
18+
"type": "LineString",
19+
"coordinates": [
20+
[-6.0150146484375, 37.38011551844836],
21+
[-5.931415557861328, 37.39702801486944]
22+
]
23+
}
24+
}
25+
]
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "Point",
9+
"coordinates": [-0.3767967224121093, 39.4689324766527]
10+
}
11+
},
12+
{
13+
"type": "Feature",
14+
"properties": {},
15+
"geometry": {
16+
"type": "LineString",
17+
"coordinates": [
18+
[-0.3689861297607422, 39.47648555419739],
19+
[-0.3595447540283203, 39.46363158174706]
20+
]
21+
}
22+
}
23+
]
24+
}

0 commit comments

Comments
 (0)