1+ import { describe , expect , it } from 'vitest' ;
2+ import {
3+ clipRouteAtProjection ,
4+ emptyRouteClippingState ,
5+ projectPositionOntoRoute ,
6+ ROUTE_GLOBAL_REMATCH_READINGS ,
7+ } from '$lib/route-clipping' ;
8+ import type { PlannedRoute , RouteLeg } from '$lib/routing' ;
9+
10+ const destination = { type : 'location' as const , lat : 0 , lng : 0 } ;
11+
12+ function route ( legs : RouteLeg [ ] ) : PlannedRoute {
13+ return {
14+ legs,
15+ totalDistance : 0 ,
16+ totalDuration : 0 ,
17+ startStationSerial : null ,
18+ endStationSerial : null ,
19+ origin : { lat : 0 , lng : 0 } ,
20+ destination,
21+ computedAt : 0 ,
22+ } ;
23+ }
24+
25+ function leg ( mode : RouteLeg [ 'mode' ] , coordinates : [ number , number ] [ ] ) : RouteLeg {
26+ return { mode, coordinates, distance : 0 , duration : 0 } ;
27+ }
28+
29+ describe ( 'route clipping' , ( ) => {
30+ it ( 'clips precisely within a sparse segment' , ( ) => {
31+ const planned = route ( [ leg ( 'foot' , [ [ 0 , 0 ] , [ 0.002 , 0 ] ] ) ] ) ;
32+ const state = projectPositionOntoRoute ( planned , { lng : 0.001 , lat : 0.00001 } , emptyRouteClippingState ( ) ) ;
33+ const clipped = clipRouteAtProjection ( planned , state . accepted ) ;
34+ expect ( clipped [ 0 ] . coordinates [ 0 ] [ 0 ] ) . toBeCloseTo ( 0.001 ) ;
35+ expect ( clipped [ 0 ] . coordinates [ 0 ] [ 1 ] ) . toBeCloseTo ( 0 ) ;
36+ expect ( clipped [ 0 ] . coordinates . at ( - 1 ) ) . toEqual ( [ 0.002 , 0 ] ) ;
37+ } ) ;
38+
39+ it ( 'removes completed legs and preserves future leg modes' , ( ) => {
40+ const planned = route ( [
41+ leg ( 'foot' , [ [ 0 , 0 ] , [ 0.001 , 0 ] ] ) ,
42+ leg ( 'bike' , [ [ 0.001 , 0 ] , [ 0.003 , 0 ] ] ) ,
43+ leg ( 'foot' , [ [ 0.003 , 0 ] , [ 0.004 , 0 ] ] ) ,
44+ ] ) ;
45+ const state = projectPositionOntoRoute ( planned , { lng : 0.002 , lat : 0 } , emptyRouteClippingState ( ) ) ;
46+ const clipped = clipRouteAtProjection ( planned , state . accepted ) ;
47+ expect ( clipped . map ( l => l . mode ) ) . toEqual ( [ 'bike' , 'foot' ] ) ;
48+ expect ( clipped [ 0 ] . coordinates [ 0 ] [ 0 ] ) . toBeCloseTo ( 0.002 ) ;
49+ } ) ;
50+
51+ it ( 'advances immediately but ignores small backwards GPS noise' , ( ) => {
52+ const planned = route ( [ leg ( 'bike' , [ [ 0 , 0 ] , [ 0.01 , 0 ] ] ) ] ) ;
53+ let state = projectPositionOntoRoute ( planned , { lng : 0.005 , lat : 0 } , emptyRouteClippingState ( ) ) ;
54+ state = projectPositionOntoRoute ( planned , { lng : 0.006 , lat : 0 } , state ) ;
55+ const forward = state . accepted ! . distanceAlongRoute ;
56+ state = projectPositionOntoRoute ( planned , { lng : 0.00598 , lat : 0 } , state ) ;
57+ expect ( state . accepted ! . distanceAlongRoute ) . toBe ( forward ) ;
58+ } ) ;
59+
60+ it ( 'restores route geometry after meaningful backwards travel' , ( ) => {
61+ const planned = route ( [ leg ( 'bike' , [ [ 0 , 0 ] , [ 0.01 , 0 ] ] ) ] ) ;
62+ let state = projectPositionOntoRoute ( planned , { lng : 0.006 , lat : 0 } , emptyRouteClippingState ( ) ) ;
63+ const before = state . accepted ! . distanceAlongRoute ;
64+ state = projectPositionOntoRoute ( planned , { lng : 0.0058 , lat : 0 } , state ) ;
65+ expect ( state . accepted ! . distanceAlongRoute ) . toBeLessThan ( before - 5 ) ;
66+ expect ( clipRouteAtProjection ( planned , state . accepted ) [ 0 ] . coordinates [ 0 ] [ 0 ] ) . toBeCloseTo ( 0.0058 ) ;
67+ } ) ;
68+
69+ it ( 'retains stable progress while off route and leaves an initially off-route route complete' , ( ) => {
70+ const planned = route ( [ leg ( 'foot' , [ [ 0 , 0 ] , [ 0.01 , 0 ] ] ) ] ) ;
71+ const initial = projectPositionOntoRoute ( planned , { lng : 0.005 , lat : 0.001 } , emptyRouteClippingState ( ) ) ;
72+ expect ( initial . accepted ) . toBeNull ( ) ;
73+ expect ( clipRouteAtProjection ( planned , initial . accepted ) [ 0 ] . coordinates ) . toEqual ( planned . legs [ 0 ] . coordinates ) ;
74+ const matched = projectPositionOntoRoute ( planned , { lng : 0.005 , lat : 0 } , initial ) ;
75+ const offRoute = projectPositionOntoRoute ( planned , { lng : 0.009 , lat : 0.001 } , matched ) ;
76+ expect ( offRoute . accepted ) . toEqual ( matched . accepted ) ;
77+ } ) ;
78+
79+ it ( 'prefers nearby route continuity at a crossing' , ( ) => {
80+ const coordinates : [ number , number ] [ ] = [ ] ;
81+ for ( let i = 0 ; i <= 25 ; i ++ ) coordinates . push ( [ i * 0.0001 , 0 ] ) ;
82+ for ( let i = 25 ; i >= 0 ; i -- ) coordinates . push ( [ i * 0.0001 , 0.00002 ] ) ;
83+ const planned = route ( [ leg ( 'bike' , coordinates ) ] ) ;
84+ let state = projectPositionOntoRoute ( planned , { lng : 0.0002 , lat : 0 } , emptyRouteClippingState ( ) ) ;
85+ state = projectPositionOntoRoute ( planned , { lng : 0.0002 , lat : 0.00002 } , state ) ;
86+ expect ( state . accepted ! . segmentOrder ) . toBeLessThanOrEqual ( 20 ) ;
87+ } ) ;
88+
89+ it ( 'requires consecutive readings before a materially better distant rematch' , ( ) => {
90+ const coordinates : [ number , number ] [ ] = [ ] ;
91+ for ( let i = 0 ; i <= 25 ; i ++ ) coordinates . push ( [ i * 0.0001 , 0 ] ) ;
92+ for ( let i = 25 ; i >= 0 ; i -- ) coordinates . push ( [ i * 0.0001 , 0.0003 ] ) ;
93+ const planned = route ( [ leg ( 'bike' , coordinates ) ] ) ;
94+ let state = projectPositionOntoRoute ( planned , { lng : 0.0002 , lat : 0 } , emptyRouteClippingState ( ) ) ;
95+ for ( let i = 1 ; i < ROUTE_GLOBAL_REMATCH_READINGS ; i ++ ) {
96+ state = projectPositionOntoRoute ( planned , { lng : 0.0002 , lat : 0.0003 } , state ) ;
97+ expect ( state . accepted ! . segmentOrder ) . toBeLessThanOrEqual ( 20 ) ;
98+ }
99+ state = projectPositionOntoRoute ( planned , { lng : 0.0002 , lat : 0.0003 } , state ) ;
100+ expect ( state . accepted ! . segmentOrder ) . toBeGreaterThan ( 20 ) ;
101+ } ) ;
102+
103+ it ( 'ignores degenerate geometry and never returns invalid lines' , ( ) => {
104+ const planned = route ( [
105+ leg ( 'foot' , [ ] ) ,
106+ leg ( 'bike' , [ [ 0 , 0 ] ] ) ,
107+ leg ( 'foot' , [ [ 0 , 0 ] , [ 0 , 0 ] ] ) ,
108+ leg ( 'bike' , [ [ 0 , 0 ] , [ 0.001 , 0 ] ] ) ,
109+ ] ) ;
110+ const state = projectPositionOntoRoute ( planned , { lng : 0.0005 , lat : 0 } , emptyRouteClippingState ( ) ) ;
111+ const clipped = clipRouteAtProjection ( planned , state . accepted ) ;
112+ expect ( clipped . every ( l => l . coordinates . length >= 2 ) ) . toBe ( true ) ;
113+ expect ( clipped ) . toHaveLength ( 1 ) ;
114+ } ) ;
115+ } ) ;
0 commit comments