1
- import { Feature , Point , LineString , MultiLineString } from "geojson" ;
2
- import { bearing } from "@turf/bearing" ;
1
+ import { Feature , Point , Position , LineString , MultiLineString } from "geojson" ;
3
2
import { distance } from "@turf/distance" ;
4
- import { destination } from "@turf/destination" ;
5
- import { lineIntersect as lineIntersects } from "@turf/line-intersect" ;
6
3
import { flattenEach } from "@turf/meta" ;
7
- import { point , lineString , Coord , Units } from "@turf/helpers" ;
8
- import { getCoords } from "@turf/invariant" ;
4
+ import { point , Coord , Units } from "@turf/helpers" ;
5
+ import { getCoord , getCoords } from "@turf/invariant" ;
9
6
10
7
/**
11
8
* Takes a {@link Point} and a {@link LineString} and calculates the closest Point on the (Multi)LineString.
@@ -51,6 +48,8 @@ function nearestPointOnLine<G extends LineString | MultiLineString>(
51
48
throw new Error ( "lines and pt are required arguments" ) ;
52
49
}
53
50
51
+ const ptPos = getCoord ( pt ) ;
52
+
54
53
let closestPt : Feature <
55
54
Point ,
56
55
{ dist : number ; index : number ; multiFeatureIndex : number ; location : number }
@@ -68,80 +67,48 @@ function nearestPointOnLine<G extends LineString | MultiLineString>(
68
67
const coords : any = getCoords ( line ) ;
69
68
70
69
for ( let i = 0 ; i < coords . length - 1 ; i ++ ) {
71
- //start
70
+ //start - start of current line section
72
71
const start : Feature < Point , { dist : number } > = point ( coords [ i ] ) ;
73
72
start . properties . dist = distance ( pt , start , options ) ;
74
- //stop
73
+ const startPos = getCoord ( start ) ;
74
+
75
+ //stop - end of current line section
75
76
const stop : Feature < Point , { dist : number } > = point ( coords [ i + 1 ] ) ;
76
77
stop . properties . dist = distance ( pt , stop , options ) ;
78
+ const stopPos = getCoord ( stop ) ;
79
+
77
80
// sectionLength
78
81
const sectionLength = distance ( start , stop , options ) ;
79
- //perpendicular
80
- const heightDistance = Math . max (
81
- start . properties . dist ,
82
- stop . properties . dist
83
- ) ;
84
- const direction = bearing ( start , stop ) ;
85
- const perpendicularPt1 = destination (
86
- pt ,
87
- heightDistance ,
88
- direction + 90 ,
89
- options
90
- ) ;
91
- const perpendicularPt2 = destination (
92
- pt ,
93
- heightDistance ,
94
- direction - 90 ,
95
- options
96
- ) ;
97
- const intersect = lineIntersects (
98
- lineString ( [
99
- perpendicularPt1 . geometry . coordinates ,
100
- perpendicularPt2 . geometry . coordinates ,
101
- ] ) ,
102
- lineString ( [ start . geometry . coordinates , stop . geometry . coordinates ] )
103
- ) ;
82
+ let intersectPos : Position ;
83
+ let wasEnd : boolean ;
84
+
85
+ // Short circuit if snap point is start or end position of the line
86
+ // segment.
87
+ if ( startPos [ 0 ] === ptPos [ 0 ] && startPos [ 1 ] === ptPos [ 1 ] ) {
88
+ [ intersectPos , , wasEnd ] = [ startPos , undefined , false ] ;
89
+ } else if ( stopPos [ 0 ] === ptPos [ 0 ] && stopPos [ 1 ] === ptPos [ 1 ] ) {
90
+ [ intersectPos , , wasEnd ] = [ stopPos , undefined , true ] ;
91
+ } else {
92
+ // Otherwise, find the nearest point the hard way.
93
+ [ intersectPos , , wasEnd ] = nearestPointOnSegment (
94
+ start . geometry . coordinates ,
95
+ stop . geometry . coordinates ,
96
+ getCoord ( pt )
97
+ ) ;
98
+ }
104
99
let intersectPt :
105
100
| Feature <
106
101
Point ,
107
102
{ dist : number ; multiFeatureIndex : number ; location : number }
108
103
>
109
104
| undefined ;
110
105
111
- if ( intersect . features . length > 0 && intersect . features [ 0 ] ) {
112
- intersectPt = {
113
- ...intersect . features [ 0 ] ,
114
- properties : {
115
- dist : distance ( pt , intersect . features [ 0 ] , options ) ,
116
- multiFeatureIndex : multiFeatureIndex ,
117
- location :
118
- length + distance ( start , intersect . features [ 0 ] , options ) ,
119
- } ,
120
- } ;
121
- }
122
-
123
- if ( start . properties . dist < closestPt . properties . dist ) {
124
- closestPt = {
125
- ...start ,
126
- properties : {
127
- ...start . properties ,
128
- index : i ,
129
- multiFeatureIndex : multiFeatureIndex ,
130
- location : length ,
131
- } ,
132
- } ;
133
- }
134
-
135
- if ( stop . properties . dist < closestPt . properties . dist ) {
136
- closestPt = {
137
- ...stop ,
138
- properties : {
139
- ...stop . properties ,
140
- index : i + 1 ,
141
- multiFeatureIndex : multiFeatureIndex ,
142
- location : length + sectionLength ,
143
- } ,
144
- } ;
106
+ if ( intersectPos ) {
107
+ intersectPt = point ( intersectPos , {
108
+ dist : distance ( pt , intersectPos , options ) ,
109
+ multiFeatureIndex : multiFeatureIndex ,
110
+ location : length + distance ( start , intersectPos , options ) ,
111
+ } ) ;
145
112
}
146
113
147
114
if (
@@ -150,9 +117,15 @@ function nearestPointOnLine<G extends LineString | MultiLineString>(
150
117
) {
151
118
closestPt = {
152
119
...intersectPt ,
153
- properties : { ...intersectPt . properties , index : i } ,
120
+ properties : {
121
+ ...intersectPt . properties ,
122
+ // Legacy behaviour where index progresses to next segment # if we
123
+ // went with the end point this iteration.
124
+ index : wasEnd ? i + 1 : i ,
125
+ } ,
154
126
} ;
155
127
}
128
+
156
129
// update length
157
130
length += sectionLength ;
158
131
}
@@ -162,5 +135,126 @@ function nearestPointOnLine<G extends LineString | MultiLineString>(
162
135
return closestPt ;
163
136
}
164
137
138
+ type Vector = [ number , number , number ] ;
139
+
140
+ function dot ( v1 : Vector , v2 : Vector ) : number {
141
+ const [ v1x , v1y , v1z ] = v1 ;
142
+ const [ v2x , v2y , v2z ] = v2 ;
143
+ return v1x * v2x + v1y * v2y + v1z * v2z ;
144
+ }
145
+
146
+ // https://en.wikipedia.org/wiki/Cross_product
147
+ function cross ( v1 : Vector , v2 : Vector ) : Vector {
148
+ const [ v1x , v1y , v1z ] = v1 ;
149
+ const [ v2x , v2y , v2z ] = v2 ;
150
+ return [ v1y * v2z - v1z * v2y , v1z * v2x - v1x * v2z , v1x * v2y - v1y * v2x ] ;
151
+ }
152
+
153
+ function magnitude ( v : Vector ) {
154
+ return Math . sqrt ( Math . pow ( v [ 0 ] , 2 ) + Math . pow ( v [ 1 ] , 2 ) + Math . pow ( v [ 2 ] , 2 ) ) ;
155
+ }
156
+
157
+ function angle ( v1 : Vector , v2 : Vector ) : number {
158
+ const theta = dot ( v1 , v2 ) / ( magnitude ( v1 ) * magnitude ( v2 ) ) ;
159
+ return Math . acos ( Math . min ( Math . max ( theta , - 1 ) , 1 ) ) ;
160
+ }
161
+
162
+ function toRadians ( degrees : number ) {
163
+ return degrees * ( Math . PI / 180 ) ;
164
+ }
165
+
166
+ function toDegrees ( radians : number ) {
167
+ return radians * ( 180 / Math . PI ) ;
168
+ }
169
+
170
+ function lngLatToVector ( a : Position ) : Vector {
171
+ const lat = toRadians ( a [ 1 ] ) ;
172
+ const lng = toRadians ( a [ 0 ] ) ;
173
+ return [
174
+ Math . cos ( lat ) * Math . cos ( lng ) ,
175
+ Math . cos ( lat ) * Math . sin ( lng ) ,
176
+ Math . sin ( lat ) ,
177
+ ] ;
178
+ }
179
+
180
+ function vectorToLngLat ( v : Vector ) : Position {
181
+ const [ x , y , z ] = v ;
182
+ const lat = toDegrees ( Math . asin ( z ) ) ;
183
+ const lng = toDegrees ( Math . atan2 ( y , x ) ) ;
184
+
185
+ return [ lng , lat ] ;
186
+ }
187
+
188
+ function nearestPointOnSegment (
189
+ posA : Position ,
190
+ posB : Position ,
191
+ posC : Position
192
+ ) : [ Position , boolean , boolean ] {
193
+ // Based heavily on this article on finding cross track distance to an arc:
194
+ // https://gis.stackexchange.com/questions/209540/projecting-cross-track-distance-on-great-circle
195
+
196
+ // Convert lng/lat to spherical coords
197
+ const A = lngLatToVector ( posA ) ; // the vector from 0,0,0 to posA
198
+ const B = lngLatToVector ( posB ) ;
199
+ const C = lngLatToVector ( posC ) ;
200
+
201
+ // Components of target point.
202
+ const [ Cx , Cy , Cz ] = C ;
203
+
204
+ // Calculate coefficients.
205
+ const [ D , E , F ] = cross ( A , B ) ;
206
+ const a = E * Cz - F * Cy ;
207
+ const b = F * Cx - D * Cz ;
208
+ const c = D * Cy - E * Cx ;
209
+
210
+ const f = c * E - b * F ;
211
+ const g = a * F - c * D ;
212
+ const h = b * D - a * E ;
213
+
214
+ const t = 1 / Math . sqrt ( Math . pow ( f , 2 ) + Math . pow ( g , 2 ) + Math . pow ( h , 2 ) ) ;
215
+
216
+ // Vectors to the two points these great circles intersect.
217
+ const I1 : Vector = [ f * t , g * t , h * t ] ;
218
+ const I2 : Vector = [ - 1 * f * t , - 1 * g * t , - 1 * h * t ] ;
219
+
220
+ // Figure out which is the closest intersection to this segment of the great
221
+ // circle.
222
+ const angleAB = angle ( A , B ) ;
223
+ const angleAI1 = angle ( A , I1 ) ;
224
+ const angleBI1 = angle ( B , I1 ) ;
225
+ const angleAI2 = angle ( A , I2 ) ;
226
+ const angleBI2 = angle ( B , I2 ) ;
227
+
228
+ let I : Vector ;
229
+
230
+ if (
231
+ ( angleAI1 < angleAI2 && angleAI1 < angleBI2 ) ||
232
+ ( angleBI1 < angleAI2 && angleBI1 < angleBI2 )
233
+ ) {
234
+ I = I1 ;
235
+ } else {
236
+ I = I2 ;
237
+ }
238
+
239
+ // I is the closest intersection to the segment, though might not actually be
240
+ // ON the segment.
241
+
242
+ // If angle AI or BI is greater than angleAB, I lies on the circle *beyond* A
243
+ // and B so use the closest of A or B as the intersection
244
+ if ( angle ( A , I ) > angleAB || angle ( B , I ) > angleAB ) {
245
+ if (
246
+ distance ( vectorToLngLat ( I ) , vectorToLngLat ( A ) ) <=
247
+ distance ( vectorToLngLat ( I ) , vectorToLngLat ( B ) )
248
+ ) {
249
+ return [ vectorToLngLat ( A ) , true , false ] ;
250
+ } else {
251
+ return [ vectorToLngLat ( B ) , false , true ] ;
252
+ }
253
+ }
254
+
255
+ // As angleAI nor angleBI don't exceed angleAB, I is on the segment
256
+ return [ vectorToLngLat ( I ) , false , false ] ;
257
+ }
258
+
165
259
export { nearestPointOnLine } ;
166
260
export default nearestPointOnLine ;
0 commit comments