@@ -5,6 +5,7 @@ import path from 'ngraph.path';
55
66import { OSMWay , OSMNode , OverpassResponse } from './types' ;
77import { isRoutableHighway } from './highwayFilter' ;
8+ import { haversineM } from './geometry' ;
89
910interface NodeData {
1011 lat : number ;
@@ -131,6 +132,63 @@ export function pointNearOrInAnyPolygon(point: [number, number], polygons: [numb
131132 return polygons . some ( polygon => pointNearOrInPolygon ( point , polygon , bufferMeters ) ) ;
132133}
133134
135+ // Projects a mid-edge endpoint click onto its closest edge and trims/extends
136+ // the trail's coords so the route ends exactly at that point.
137+ //
138+ // The Eulerian circuit's traversal order isn't dictated by click order — with
139+ // a long/complex route, a street shared with an earlier segment (e.g. the
140+ // endpoint clicked near the route's own start, a normal way to close a loop)
141+ // can appear only once, and much earlier than the real end. Truncating there
142+ // would silently discard the rest of the route down to just a handful of
143+ // points, so a match is only trusted as the true terminus when it's actually
144+ // near the tail.
145+ export function applyEndpointSnap (
146+ coords : { lat : number ; lon : number ; hasConstruction ?: boolean } [ ] ,
147+ endPoint : { lat : number ; lon : number } ,
148+ snap : { lat : number ; lon : number ; u : string ; v : string } ,
149+ uCoord : { lat : number ; lon : number } ,
150+ vCoord : { lat : number ; lon : number } ,
151+ ) : { lat : number ; lon : number ; hasConstruction ?: boolean } [ ] {
152+ const distToSnap = haversineM ( endPoint . lat , endPoint . lon , snap . lat , snap . lon ) ;
153+ const distToU = haversineM ( endPoint . lat , endPoint . lon , uCoord . lat , uCoord . lon ) ;
154+ const distToV = haversineM ( endPoint . lat , endPoint . lon , vCoord . lat , vCoord . lon ) ;
155+ const minNodeDist = Math . min ( distToU , distToV ) ;
156+
157+ // Only truncate when the click is genuinely mid-edge, not just slightly
158+ // off an intersection due to map rounding. ~5m gap between node and edge snap.
159+ const NODE_THRESHOLD_M = 5 ;
160+ if ( minNodeDist - distToSnap <= NODE_THRESHOLD_M ) return coords ;
161+
162+ const eq = ( a : number , b : number ) => Math . abs ( a - b ) < 1e-9 ;
163+ const isU = ( c : { lat : number , lon : number } ) => eq ( c . lat , uCoord . lat ) && eq ( c . lon , uCoord . lon ) ;
164+ const isV = ( c : { lat : number , lon : number } ) => eq ( c . lat , vCoord . lat ) && eq ( c . lon , vCoord . lon ) ;
165+
166+ // Find the last time the route traversed the snap edge.
167+ let lastIdx = - 1 ;
168+ for ( let i = 0 ; i < coords . length - 1 ; i ++ ) {
169+ const a = coords [ i ] , b = coords [ i + 1 ] ;
170+ if ( ( isU ( a ) && isV ( b ) ) || ( isV ( a ) && isU ( b ) ) ) lastIdx = i ;
171+ }
172+
173+ const NEAR_END_FRACTION = 0.1 ;
174+ const allowedFromEnd = Math . max ( 5 , Math . ceil ( coords . length * NEAR_END_FRACTION ) ) ;
175+ const isNearEnd = lastIdx !== - 1 && ( coords . length - 1 - lastIdx ) <= allowedFromEnd ;
176+ if ( isNearEnd ) {
177+ // Truncate at the snap point during that traversal.
178+ return [ ...coords . slice ( 0 , lastIdx + 1 ) , { lat : snap . lat , lon : snap . lon } ] ;
179+ }
180+
181+ // The snap edge either wasn't traversed, or only appeared far from the
182+ // end. If the trail's final node is one of the snap edge's endpoints,
183+ // extend along that edge to the snap point; otherwise leave coords as-is
184+ // rather than risk discarding most of the route.
185+ const last = coords [ coords . length - 1 ] ;
186+ if ( isU ( last ) || isV ( last ) ) {
187+ return [ ...coords , { lat : snap . lat , lon : snap . lon } ] ;
188+ }
189+ return coords ;
190+ }
191+
134192// The entry bridge (approach → area sweep, mixed mode) carries no coverage
135193// guarantee — only the area's own CPP-solved path does. So any edge the
136194// bridge shares with the path's own leading edges is pure redundant mileage:
@@ -2084,42 +2142,7 @@ export class StreetGraph {
20842142 const uNode = this . graph . getNode ( snap . u ) ;
20852143 const vNode = this . graph . getNode ( snap . v ) ;
20862144 if ( uNode && vNode ) {
2087- const distToSnap = this . haversine ( endPoint . lat , endPoint . lon , snap . lat , snap . lon ) ;
2088- const distToU = this . haversine ( endPoint . lat , endPoint . lon , uNode . data . lat , uNode . data . lon ) ;
2089- const distToV = this . haversine ( endPoint . lat , endPoint . lon , vNode . data . lat , vNode . data . lon ) ;
2090- const minNodeDist = Math . min ( distToU , distToV ) ;
2091-
2092- // Only truncate when the click is genuinely mid-edge,
2093- // not just slightly off an intersection due to map
2094- // rounding. ~5m gap between node and edge snap.
2095- const NODE_THRESHOLD_M = 5 ;
2096- if ( minNodeDist - distToSnap > NODE_THRESHOLD_M ) {
2097- const eq = ( a : number , b : number ) => Math . abs ( a - b ) < 1e-9 ;
2098- const isU = ( c : { lat : number , lon : number } ) =>
2099- eq ( c . lat , uNode . data . lat ) && eq ( c . lon , uNode . data . lon ) ;
2100- const isV = ( c : { lat : number , lon : number } ) =>
2101- eq ( c . lat , vNode . data . lat ) && eq ( c . lon , vNode . data . lon ) ;
2102-
2103- // Find the last time the route traversed the snap edge.
2104- let lastIdx = - 1 ;
2105- for ( let i = 0 ; i < coords . length - 1 ; i ++ ) {
2106- const a = coords [ i ] , b = coords [ i + 1 ] ;
2107- if ( ( isU ( a ) && isV ( b ) ) || ( isV ( a ) && isU ( b ) ) ) lastIdx = i ;
2108- }
2109-
2110- if ( lastIdx !== - 1 ) {
2111- // Truncate at the snap point during that traversal.
2112- return [ ...coords . slice ( 0 , lastIdx + 1 ) , { lat : snap . lat , lon : snap . lon } ] ;
2113- }
2114-
2115- // The snap edge was not traversed. If the trail's
2116- // final node is one of the snap edge's endpoints,
2117- // extend along that edge to the snap point.
2118- const last = coords [ coords . length - 1 ] ;
2119- if ( isU ( last ) || isV ( last ) ) {
2120- coords . push ( { lat : snap . lat , lon : snap . lon } ) ;
2121- }
2122- }
2145+ return applyEndpointSnap ( coords , endPoint , snap , uNode . data , vNode . data ) ;
21232146 }
21242147 }
21252148 }
0 commit comments