@tije-syntra/snap-to-line 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,3164 @@
1
+ 'use strict';
2
+
3
+ // src/types.ts
4
+ var DirectionOutbound = "outbound";
5
+ var DirectionInbound = "inbound";
6
+ var DirectionLoop = "loop";
7
+ var DirectionUnknown = "unknown";
8
+ function DefaultConfig() {
9
+ return {
10
+ MaxSnapDistanceMeter: 60,
11
+ CandidateLimit: 8,
12
+ Looping: false,
13
+ UseBearingValidation: true,
14
+ MaxBearingDiffDegree: 60,
15
+ UseMovementBearing: true,
16
+ MinMovementMeter: 8,
17
+ UseTripDirection: false,
18
+ TripDirection: DirectionUnknown,
19
+ AllowSameStartEndStop: true,
20
+ LoopClosureToleranceMeter: 10,
21
+ UseSpeed: true,
22
+ PreventBackwardTransition: false,
23
+ MeasureRegressionToleranceMeter: 0,
24
+ ClampBackwardMinConfidence: 0,
25
+ ClampDwellSpeedKmh: 0,
26
+ MeasureAdvanceSlackMeter: 0,
27
+ SegmentSwitchHysteresisLog: 0,
28
+ SnappedJumpSlackMeter: 0,
29
+ HoldLastSegmentOnMiss: false,
30
+ HoldLastSegmentMaxDistM: 0,
31
+ HoldLastSegmentMaxAgeMs: 0,
32
+ HoldLastSegmentMinConfidence: 0,
33
+ WildGPSStabilize: false,
34
+ WildGPSJumpMinMeter: 0,
35
+ WildGPSJumpMultiplier: 0,
36
+ WildGPSMaxAdvanceFactor: 0,
37
+ MaxForwardSnapMeter: 0,
38
+ NoBackwardSnap: false,
39
+ RequireNextStopBeforeSegmentSwitch: false,
40
+ NextStopPassToleranceMeter: 0,
41
+ RequireStopRadiusForSegmentSwitch: false,
42
+ SegmentSwitchStopRadiusMeter: 0,
43
+ FoldedSegmentBranchLock: false,
44
+ FoldedSegmentMinViable: 0,
45
+ BranchLockSearchWindowM: 0,
46
+ BranchUnlockNormalTicks: 0,
47
+ FoldedSegmentMeasureSpreadM: 0,
48
+ SnapContinuityFromPrevious: false,
49
+ SnapDistanceResetOnGrow: false,
50
+ SnapDistanceGrowResetTicks: 0,
51
+ SnapDistanceGrowMinDeltaM: 0,
52
+ SnapDistanceResetMinMeter: 0,
53
+ SnapDistanceResetMaxMeter: 0
54
+ };
55
+ }
56
+
57
+ // src/errors.ts
58
+ var SnapToLineError = class extends Error {
59
+ constructor(message) {
60
+ super(message);
61
+ this.name = "SnapToLineError";
62
+ }
63
+ };
64
+ var ErrInsufficientStops = new SnapToLineError(
65
+ "snaptoline: at least two stops required"
66
+ );
67
+ var ErrInvalidLine = new SnapToLineError(
68
+ "snaptoline: line must contain at least two points"
69
+ );
70
+ var ErrStopMeasureNotMonotonic = new SnapToLineError(
71
+ "snaptoline: projected stop measures must be monotonically increasing"
72
+ );
73
+ var ErrInvalidMeasureRange = new SnapToLineError(
74
+ "snaptoline: invalid measure range"
75
+ );
76
+ var ErrNoCandidates = new SnapToLineError(
77
+ "snaptoline: no snap candidates found"
78
+ );
79
+ var ErrEmptySegments = new SnapToLineError(
80
+ "snaptoline: no segments built from stops"
81
+ );
82
+
83
+ // src/mathgeo/distance.ts
84
+ var EARTH_RADIUS_METER = 63710088e-1;
85
+ function toRad(deg) {
86
+ return deg * Math.PI / 180;
87
+ }
88
+ function toDeg(rad) {
89
+ return rad * 180 / Math.PI;
90
+ }
91
+ function distanceMeter(a, b) {
92
+ const lat1 = toRad(a[1]);
93
+ const lat2 = toRad(b[1]);
94
+ const dLat = toRad(b[1] - a[1]);
95
+ const dLon = toRad(b[0] - a[0]);
96
+ const sinDLat = Math.sin(dLat / 2);
97
+ const sinDLon = Math.sin(dLon / 2);
98
+ const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
99
+ return 2 * EARTH_RADIUS_METER * Math.asin(Math.min(1, Math.sqrt(h)));
100
+ }
101
+
102
+ // src/mathgeo/bearing.ts
103
+ function bearingBetween(a, b) {
104
+ const lat1 = toRad(a[1]);
105
+ const lat2 = toRad(b[1]);
106
+ const dLon = toRad(b[0] - a[0]);
107
+ const y = Math.sin(dLon) * Math.cos(lat2);
108
+ const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
109
+ let bearing = toDeg(Math.atan2(y, x));
110
+ if (bearing < 0) {
111
+ bearing += 360;
112
+ }
113
+ return bearing;
114
+ }
115
+ function bearingDiff(a, b) {
116
+ let diff = Math.abs(a - b);
117
+ if (diff > 180) {
118
+ diff = 360 - diff;
119
+ }
120
+ return diff;
121
+ }
122
+
123
+ // src/mathgeo/measure.ts
124
+ var ErrInvalidLine2 = new Error(
125
+ "mathgeo: line must contain at least two points"
126
+ );
127
+ var ErrInvalidMeasureRange2 = new Error(
128
+ "mathgeo: invalid measure range"
129
+ );
130
+ function lineLengthMeter(line) {
131
+ if (line.length < 2) {
132
+ return 0;
133
+ }
134
+ let total = 0;
135
+ for (let i = 0; i < line.length - 1; i++) {
136
+ total += distanceMeter(line[i], line[i + 1]);
137
+ }
138
+ return total;
139
+ }
140
+ function pointAtMeasure(line, measure) {
141
+ if (line.length === 0) {
142
+ return [[0, 0], 0];
143
+ }
144
+ if (line.length === 1 || measure <= 0) {
145
+ return [line[0], 0];
146
+ }
147
+ const total = lineLengthMeter(line);
148
+ if (measure >= total) {
149
+ return [line[line.length - 1], line.length - 2];
150
+ }
151
+ let cumulative = 0;
152
+ for (let i = 0; i < line.length - 1; i++) {
153
+ const a = line[i];
154
+ const b = line[i + 1];
155
+ const segLen = distanceMeter(a, b);
156
+ if (cumulative + segLen >= measure) {
157
+ let t = (measure - cumulative) / segLen;
158
+ if (t < 0) t = 0;
159
+ else if (t > 1) t = 1;
160
+ return [[a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])], i];
161
+ }
162
+ cumulative += segLen;
163
+ }
164
+ return [line[line.length - 1], line.length - 2];
165
+ }
166
+ function bearingAtMeasure(line, measure) {
167
+ if (line.length < 2) {
168
+ return 0;
169
+ }
170
+ const total = lineLengthMeter(line);
171
+ if (measure >= total) {
172
+ const a2 = line[line.length - 2];
173
+ const b2 = line[line.length - 1];
174
+ return bearingBetween(a2, b2);
175
+ }
176
+ let cumulative = 0;
177
+ for (let i = 0; i < line.length - 1; i++) {
178
+ const a2 = line[i];
179
+ const b2 = line[i + 1];
180
+ const segLen = distanceMeter(a2, b2);
181
+ if (cumulative + segLen >= measure || i === line.length - 2) {
182
+ return bearingBetween(a2, b2);
183
+ }
184
+ cumulative += segLen;
185
+ }
186
+ const a = line[line.length - 2];
187
+ const b = line[line.length - 1];
188
+ return bearingBetween(a, b);
189
+ }
190
+ function sliceLineByMeasure(line, fromMeasure, toMeasure) {
191
+ if (line.length < 2) {
192
+ throw ErrInvalidLine2;
193
+ }
194
+ if (toMeasure <= fromMeasure) {
195
+ throw ErrInvalidMeasureRange2;
196
+ }
197
+ const [startPoint, startIdx] = pointAtMeasure(line, fromMeasure);
198
+ const [endPoint, endIdx] = pointAtMeasure(line, toMeasure);
199
+ const result = [startPoint];
200
+ for (let i = startIdx + 1; i <= endIdx && i < line.length; i++) {
201
+ result.push(line[i]);
202
+ }
203
+ const last = result[result.length - 1];
204
+ if (last[0] !== endPoint[0] || last[1] !== endPoint[1]) {
205
+ result.push(endPoint);
206
+ }
207
+ if (result.length < 2) {
208
+ throw ErrInvalidMeasureRange2;
209
+ }
210
+ return result;
211
+ }
212
+
213
+ // src/mathgeo/projection.ts
214
+ function projectPointOnSegment(a, b, p) {
215
+ const ax = a[0];
216
+ const ay = a[1];
217
+ const bx = b[0];
218
+ const by = b[1];
219
+ const px = p[0];
220
+ const py = p[1];
221
+ const dx = bx - ax;
222
+ const dy = by - ay;
223
+ if (dx === 0 && dy === 0) {
224
+ return [a, 0];
225
+ }
226
+ let t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
227
+ if (t < 0) t = 0;
228
+ else if (t > 1) t = 1;
229
+ const projected = [ax + t * dx, ay + t * dy];
230
+ return [projected, t];
231
+ }
232
+ function findProjectionCandidates(line, point) {
233
+ if (line.length < 2) {
234
+ return [];
235
+ }
236
+ let cumulative = 0;
237
+ const candidates = [];
238
+ for (let i = 0; i < line.length - 1; i++) {
239
+ const a = line[i];
240
+ const b = line[i + 1];
241
+ const segLen = distanceMeter(a, b);
242
+ const [projected, t] = projectPointOnSegment(a, b, point);
243
+ const measure = cumulative + segLen * t;
244
+ candidates.push({
245
+ Point: projected,
246
+ Measure: measure,
247
+ LineIndex: i,
248
+ DistanceMeter: distanceMeter(point, projected)
249
+ });
250
+ cumulative += segLen;
251
+ }
252
+ return candidates;
253
+ }
254
+ function findNearestProjectionAfterMeasure(line, point, minMeasure) {
255
+ const candidates = findProjectionCandidates(line, point);
256
+ let best = null;
257
+ for (const c of candidates) {
258
+ if (c.Measure < minMeasure) {
259
+ continue;
260
+ }
261
+ if (!best || c.DistanceMeter < best.DistanceMeter) {
262
+ best = c;
263
+ }
264
+ }
265
+ if (best) {
266
+ return best;
267
+ }
268
+ const total = lineLengthMeter(line);
269
+ const last = line[line.length - 1];
270
+ return {
271
+ Point: last,
272
+ Measure: total,
273
+ LineIndex: line.length - 2,
274
+ DistanceMeter: distanceMeter(point, last)
275
+ };
276
+ }
277
+ function findProjectionNearMeasure(line, point, targetMeasure, searchWindow) {
278
+ const candidates = findProjectionCandidates(line, point);
279
+ let best = null;
280
+ for (const c of candidates) {
281
+ let delta = c.Measure - targetMeasure;
282
+ if (delta < 0) delta = -delta;
283
+ if (searchWindow > 0 && delta > searchWindow) {
284
+ continue;
285
+ }
286
+ if (!best || c.DistanceMeter < best.DistanceMeter) {
287
+ best = c;
288
+ }
289
+ }
290
+ if (best) {
291
+ return best;
292
+ }
293
+ return findNearestProjectionAfterMeasure(line, point, 0);
294
+ }
295
+
296
+ // src/geometry.ts
297
+ var DEFAULT_SNAPPED_JUMP_SLACK_METER = 4;
298
+ function DistanceMeter(a, b) {
299
+ return distanceMeter(a, b);
300
+ }
301
+ function LineLengthMeter(line) {
302
+ return lineLengthMeter(line);
303
+ }
304
+ function BearingBetween(a, b) {
305
+ return bearingBetween(a, b);
306
+ }
307
+ function BearingDiff(a, b) {
308
+ return bearingDiff(a, b);
309
+ }
310
+ function BearingAtMeasure(line, measure) {
311
+ return bearingAtMeasure(line, measure);
312
+ }
313
+ function SliceLineByMeasure(line, fromMeasure, toMeasure) {
314
+ try {
315
+ return sliceLineByMeasure(line, fromMeasure, toMeasure);
316
+ } catch (e) {
317
+ if (e instanceof Error && e.message.includes("invalid measure range")) {
318
+ throw ErrInvalidMeasureRange;
319
+ }
320
+ throw ErrInvalidLine;
321
+ }
322
+ }
323
+ function PointAtMeasure(line, measure) {
324
+ return pointAtMeasure(line, measure);
325
+ }
326
+ function FindProjectionCandidates(line, point) {
327
+ return findProjectionCandidates(line, point);
328
+ }
329
+ function FindNearestProjectionAfterMeasure(line, point, minMeasure) {
330
+ return findNearestProjectionAfterMeasure(line, point, minMeasure);
331
+ }
332
+ function FindProjectionNearMeasure(line, point, targetMeasure, searchWindow) {
333
+ return findProjectionNearMeasure(line, point, targetMeasure, searchWindow);
334
+ }
335
+ function ProjectPointOnLine(line, point) {
336
+ const candidates = FindProjectionCandidates(line, point);
337
+ if (candidates.length === 0) {
338
+ return {
339
+ Point: [0, 0],
340
+ Measure: 0,
341
+ LineIndex: 0,
342
+ DistanceMeter: 0
343
+ };
344
+ }
345
+ let best = candidates[0];
346
+ for (const c of candidates.slice(1)) {
347
+ if (c.DistanceMeter < best.DistanceMeter) {
348
+ best = c;
349
+ }
350
+ }
351
+ return best;
352
+ }
353
+ function measureDelta(a, b) {
354
+ const d = a - b;
355
+ return d < 0 ? -d : d;
356
+ }
357
+ function hasLastSnapped(p) {
358
+ return p[0] !== 0 || p[1] !== 0;
359
+ }
360
+ function pickMeasureContinuityCandidate(viable, prevRelMeasure, lastSnapped) {
361
+ let best = viable[0];
362
+ const useSnapped = hasLastSnapped(lastSnapped);
363
+ for (const c of viable.slice(1)) {
364
+ const dBest = measureDelta(best.Measure, prevRelMeasure);
365
+ const dCand = measureDelta(c.Measure, prevRelMeasure);
366
+ if (dCand + 1 < dBest) {
367
+ best = c;
368
+ } else if (dCand <= dBest + 1 && useSnapped) {
369
+ if (distanceMeter(lastSnapped, c.Point) < distanceMeter(lastSnapped, best.Point)) {
370
+ best = c;
371
+ }
372
+ } else if (dCand === dBest && !useSnapped && c.DistanceMeter < best.DistanceMeter) {
373
+ best = c;
374
+ }
375
+ }
376
+ return best;
377
+ }
378
+ function ProjectPointOnLineContinued(line, point, prevRelMeasure, prevPoint, lastSnapped, cfg) {
379
+ if (prevRelMeasure <= 0) {
380
+ return ProjectPointOnLine(line, point);
381
+ }
382
+ const candidates = FindProjectionCandidates(line, point);
383
+ const viable = candidates.filter(
384
+ (c) => c.DistanceMeter <= cfg.MaxSnapDistanceMeter
385
+ );
386
+ if (viable.length === 0) {
387
+ return ProjectPointOnLine(line, point);
388
+ }
389
+ if (viable.length === 1) {
390
+ return viable[0];
391
+ }
392
+ let tol = cfg.MeasureRegressionToleranceMeter;
393
+ if (tol <= 0) {
394
+ tol = 30;
395
+ }
396
+ const best = pickMeasureContinuityCandidate(
397
+ viable,
398
+ prevRelMeasure,
399
+ lastSnapped
400
+ );
401
+ const nearest = ProjectPointOnLine(line, point);
402
+ if (nearest.Measure < prevRelMeasure - tol && measureDelta(best.Measure, prevRelMeasure) + 5 < measureDelta(nearest.Measure, prevRelMeasure)) {
403
+ return best;
404
+ }
405
+ let jumpSlack = cfg.SnappedJumpSlackMeter;
406
+ if (jumpSlack <= 0) {
407
+ jumpSlack = DEFAULT_SNAPPED_JUMP_SLACK_METER;
408
+ }
409
+ let movement = 0;
410
+ if (prevPoint) {
411
+ movement = distanceMeter(prevPoint.Point, point);
412
+ }
413
+ let minMovement = cfg.MinMovementMeter;
414
+ if (minMovement <= 0) {
415
+ minMovement = 1;
416
+ }
417
+ if (prevPoint && movement < minMovement && hasLastSnapped(lastSnapped)) {
418
+ return best;
419
+ }
420
+ if (hasLastSnapped(lastSnapped)) {
421
+ if (movement < 1) {
422
+ movement = 1;
423
+ }
424
+ const jumpNearest = distanceMeter(lastSnapped, nearest.Point);
425
+ const jumpBest = distanceMeter(lastSnapped, best.Point);
426
+ if (jumpNearest > movement * 0.75 + jumpSlack && jumpBest < jumpNearest) {
427
+ return best;
428
+ }
429
+ if (jumpNearest > jumpSlack && jumpBest <= jumpSlack) {
430
+ return best;
431
+ }
432
+ const dBest = measureDelta(best.Measure, prevRelMeasure);
433
+ const dNearest = measureDelta(nearest.Measure, prevRelMeasure);
434
+ if (dBest <= dNearest + tol / 2 && jumpBest <= jumpNearest) {
435
+ return best;
436
+ }
437
+ }
438
+ if (measureDelta(best.Measure, prevRelMeasure) + 1 < measureDelta(nearest.Measure, prevRelMeasure)) {
439
+ return best;
440
+ }
441
+ return nearest;
442
+ }
443
+ function ForwardProjectionOnSegment(line, point, minRelMeasure, maxRelMeasure, maxDist) {
444
+ if (line.length < 2) {
445
+ return [
446
+ { Point: [0, 0], Measure: 0, LineIndex: 0, DistanceMeter: 0 },
447
+ false
448
+ ];
449
+ }
450
+ const cands = FindProjectionCandidates(line, point);
451
+ let best = null;
452
+ for (const c of cands) {
453
+ if (c.DistanceMeter > maxDist) continue;
454
+ if (c.Measure < minRelMeasure || c.Measure > maxRelMeasure) continue;
455
+ if (!best || c.DistanceMeter < best.DistanceMeter) {
456
+ best = c;
457
+ }
458
+ }
459
+ if (!best) {
460
+ return [
461
+ { Point: [0, 0], Measure: 0, LineIndex: 0, DistanceMeter: 0 },
462
+ false
463
+ ];
464
+ }
465
+ return [best, true];
466
+ }
467
+ function nearestStopID(stops, point) {
468
+ if (stops.length === 0) {
469
+ return "";
470
+ }
471
+ let best = stops[0];
472
+ let bestDist = distanceMeter(point, best.Point);
473
+ for (const stop of stops.slice(1)) {
474
+ const d = distanceMeter(point, stop.Point);
475
+ if (d < bestDist) {
476
+ best = stop;
477
+ bestDist = d;
478
+ }
479
+ }
480
+ return best.ID;
481
+ }
482
+ function segmentProgress(seg, measure) {
483
+ const length = seg.ToMeasure - seg.FromMeasure;
484
+ if (length <= 0) {
485
+ return 0;
486
+ }
487
+ return (measure - seg.FromMeasure) / length;
488
+ }
489
+
490
+ // src/loop.ts
491
+ function IsSameStop(a, b, toleranceMeter) {
492
+ if (a.ID !== b.ID) {
493
+ return false;
494
+ }
495
+ return DistanceMeter(a.Point, b.Point) <= toleranceMeter;
496
+ }
497
+ function sortStopsByOrder(stops) {
498
+ return [...stops].sort((a, b) => a.Order - b.Order);
499
+ }
500
+ function CountOccurrence(projected, stopID) {
501
+ let count = 0;
502
+ for (const p of projected) {
503
+ if (p.Stop.ID === stopID) {
504
+ count++;
505
+ }
506
+ }
507
+ return count;
508
+ }
509
+ function ProjectStopNearMeasure(line, stop, targetMeasure, _cfg) {
510
+ const total = LineLengthMeter(line);
511
+ let window = total * 0.15;
512
+ if (window < 50) window = 50;
513
+ if (window > 500) window = 500;
514
+ const candidate = FindProjectionNearMeasure(
515
+ line,
516
+ stop.Point,
517
+ targetMeasure,
518
+ window
519
+ );
520
+ return {
521
+ Stop: stop,
522
+ Measure: candidate.Measure,
523
+ LineIndex: candidate.LineIndex,
524
+ Occurrence: 0
525
+ };
526
+ }
527
+ function ProjectStopForwardOnly(line, stop, minMeasure, _cfg) {
528
+ const candidates = FindProjectionCandidates(line, stop.Point);
529
+ let best = null;
530
+ for (const c of candidates) {
531
+ if (c.Measure < minMeasure) continue;
532
+ if (!best || c.DistanceMeter < best.DistanceMeter) {
533
+ best = c;
534
+ }
535
+ }
536
+ if (!best) {
537
+ best = FindNearestProjectionAfterMeasure(line, stop.Point, minMeasure);
538
+ }
539
+ return {
540
+ Stop: stop,
541
+ Measure: best.Measure,
542
+ LineIndex: best.LineIndex,
543
+ Occurrence: 0
544
+ };
545
+ }
546
+ function ProjectStopsSequential(line, stops, cfg) {
547
+ if (stops.length < 2) {
548
+ throw ErrInsufficientStops;
549
+ }
550
+ if (line.length < 2) {
551
+ throw ErrInvalidLine;
552
+ }
553
+ const sorted = sortStopsByOrder(stops);
554
+ const totalLength = LineLengthMeter(line);
555
+ const result = [];
556
+ const firstStop = sorted[0];
557
+ const lastStop = sorted[sorted.length - 1];
558
+ const isClosedLoopStop = cfg.Looping && cfg.AllowSameStartEndStop && IsSameStop(firstStop, lastStop, cfg.LoopClosureToleranceMeter);
559
+ let lastMeasure = 0;
560
+ for (let i = 0; i < sorted.length; i++) {
561
+ const stop = sorted[i];
562
+ let projected;
563
+ if (i === 0) {
564
+ projected = ProjectStopNearMeasure(line, stop, 0);
565
+ projected.Measure = 0;
566
+ projected.Occurrence = 1;
567
+ } else if (isClosedLoopStop && i === sorted.length - 1) {
568
+ projected = ProjectStopNearMeasure(line, stop, totalLength);
569
+ projected.Measure = totalLength;
570
+ projected.Occurrence = 2;
571
+ projected.IsLoopClosure = true;
572
+ } else {
573
+ projected = ProjectStopForwardOnly(line, stop, lastMeasure);
574
+ projected.Occurrence = CountOccurrence(result, stop.ID) + 1;
575
+ }
576
+ if (projected.Measure < lastMeasure) {
577
+ throw ErrStopMeasureNotMonotonic;
578
+ }
579
+ result.push(projected);
580
+ lastMeasure = projected.Measure;
581
+ }
582
+ return result;
583
+ }
584
+ function DetectLoopClosure(projected) {
585
+ if (projected.length < 2) {
586
+ return false;
587
+ }
588
+ return !!projected[projected.length - 1].IsLoopClosure;
589
+ }
590
+ function segmentID(from, to, order) {
591
+ return `SEG-${from.ID}-${to.ID}-${order}`;
592
+ }
593
+ function detectLoopFromStops(stops, allowSame, toleranceM) {
594
+ if (!allowSame) {
595
+ return false;
596
+ }
597
+ const sorted = sortStopsByOrder(stops);
598
+ if (sorted.length < 2) {
599
+ return false;
600
+ }
601
+ const first = sorted[0];
602
+ const last = sorted[sorted.length - 1];
603
+ return IsSameStop(first, last, toleranceM);
604
+ }
605
+
606
+ // src/segment.ts
607
+ function resolveSegmentDirection(cfg, _isLoopClosure, segmentIndex) {
608
+ if (segmentIndex >= 0 && cfg.SegmentDirections && segmentIndex < cfg.SegmentDirections.length) {
609
+ const dir = cfg.SegmentDirections[segmentIndex];
610
+ if (dir && dir !== "unknown") {
611
+ return dir;
612
+ }
613
+ }
614
+ if (cfg.Looping) {
615
+ return "loop";
616
+ }
617
+ if (cfg.UseTripDirection && cfg.TripDirection !== "unknown") {
618
+ return cfg.TripDirection;
619
+ }
620
+ return "unknown";
621
+ }
622
+ function BuildSegmentsFromProjectedStops(line, projectedStops, cfg) {
623
+ if (projectedStops.length < 2) {
624
+ throw ErrInsufficientStops;
625
+ }
626
+ const segments = [];
627
+ const totalLength = LineLengthMeter(line);
628
+ for (let i = 0; i < projectedStops.length - 1; i++) {
629
+ const from = projectedStops[i];
630
+ const to = projectedStops[i + 1];
631
+ const fromMeasure = from.Measure;
632
+ const toMeasure = to.Measure;
633
+ if (toMeasure <= fromMeasure) {
634
+ throw ErrInvalidMeasureRange;
635
+ }
636
+ const geometry = SliceLineByMeasure(line, fromMeasure, toMeasure);
637
+ let bearing = BearingAtMeasure(geometry, 0);
638
+ if (geometry.length >= 2) {
639
+ bearing = BearingBetween(geometry[0], geometry[geometry.length - 1]);
640
+ }
641
+ const direction = resolveSegmentDirection(cfg, !!to.IsLoopClosure, i);
642
+ segments.push({
643
+ ID: segmentID(from.Stop, to.Stop, i + 1),
644
+ FromStop: from.Stop,
645
+ ToStop: to.Stop,
646
+ FromMeasure: fromMeasure,
647
+ ToMeasure: toMeasure,
648
+ Geometry: geometry,
649
+ Order: i + 1,
650
+ Direction: direction,
651
+ Bearing: bearing,
652
+ IsLooping: cfg.Looping,
653
+ IsLoopClosing: !!to.IsLoopClosure
654
+ });
655
+ }
656
+ if (cfg.Looping && segments.length > 0) {
657
+ const last = segments[segments.length - 1];
658
+ if (last.ToMeasure < totalLength * 0.95 && projectedStops[projectedStops.length - 1].IsLoopClosure) {
659
+ throw new Error(
660
+ "snaptoline: loop closing segment must reach end of line"
661
+ );
662
+ }
663
+ }
664
+ return segments;
665
+ }
666
+ function BuildSegments(line, stops, cfg) {
667
+ const projected = ProjectStopsSequential(line, stops, cfg);
668
+ return BuildSegmentsFromProjectedStops(line, projected, cfg);
669
+ }
670
+ function segmentWithOrder(segments, order) {
671
+ for (const seg of segments) {
672
+ if (seg.Order === order) {
673
+ return [seg, true];
674
+ }
675
+ }
676
+ return [
677
+ {
678
+ ID: "",
679
+ FromStop: { ID: "", Point: [0, 0], Order: 0 },
680
+ ToStop: { ID: "", Point: [0, 0], Order: 0 },
681
+ FromMeasure: 0,
682
+ ToMeasure: 0,
683
+ Geometry: [],
684
+ Order: 0,
685
+ Direction: "unknown",
686
+ Bearing: 0,
687
+ IsLooping: false,
688
+ IsLoopClosing: false
689
+ },
690
+ false
691
+ ];
692
+ }
693
+
694
+ // src/direction.ts
695
+ function DirectionScore(diff, maxDiff) {
696
+ if (maxDiff <= 0) {
697
+ return 1;
698
+ }
699
+ if (diff > maxDiff) {
700
+ return 0.05;
701
+ }
702
+ return 1 - diff / maxDiff;
703
+ }
704
+ function TripDirectionScore(candidate, active) {
705
+ if (active === "unknown") {
706
+ return 1;
707
+ }
708
+ if (candidate === active) {
709
+ return 1;
710
+ }
711
+ if (candidate === "loop") {
712
+ return 0.7;
713
+ }
714
+ return 0.05;
715
+ }
716
+ function EmissionScore(distanceMeter2, maxDistance) {
717
+ if (maxDistance <= 0) {
718
+ return 0.05;
719
+ }
720
+ if (distanceMeter2 > maxDistance) {
721
+ return 0.05;
722
+ }
723
+ const ratio = distanceMeter2 / maxDistance;
724
+ const score = 1 - ratio;
725
+ if (score < 0.05) {
726
+ return 0.05;
727
+ }
728
+ return score;
729
+ }
730
+ function resolveBusBearing(point, prev, cfg) {
731
+ if (point.Bearing != null && point.Bearing > 0) {
732
+ return [point.Bearing, true];
733
+ }
734
+ if (!prev || !cfg.UseMovementBearing) {
735
+ return [0, false];
736
+ }
737
+ const movement = distanceMeter(prev.Point, point.Point);
738
+ if (movement < cfg.MinMovementMeter) {
739
+ return [0, false];
740
+ }
741
+ return [bearingBetween(prev.Point, point.Point), true];
742
+ }
743
+ function shouldWeakenDirectionValidation(point, prev, cfg) {
744
+ const lowSpeedKmh = 3;
745
+ if (cfg.UseSpeed && (point.Speed ?? 0) < lowSpeedKmh) {
746
+ return true;
747
+ }
748
+ if (prev && cfg.UseMovementBearing) {
749
+ const movement = distanceMeter(prev.Point, point.Point);
750
+ if (movement < cfg.MinMovementMeter) {
751
+ return true;
752
+ }
753
+ }
754
+ return false;
755
+ }
756
+ function scoreDirection(busBearing, hasBearing, lineBearing, cfg, weaken) {
757
+ if (!cfg.UseBearingValidation || !hasBearing) {
758
+ return [1, 0];
759
+ }
760
+ const diff = bearingDiff(busBearing, lineBearing);
761
+ let score = DirectionScore(diff, cfg.MaxBearingDiffDegree);
762
+ if (weaken && score < 0.7) {
763
+ score = 0.7;
764
+ }
765
+ return [score, diff];
766
+ }
767
+ function logScore(score) {
768
+ if (score <= 0) {
769
+ return Math.log(0.05);
770
+ }
771
+ return Math.log(score);
772
+ }
773
+ function clampConfidence(v) {
774
+ return Math.max(0, Math.min(1, v));
775
+ }
776
+ function confidenceFromScores(c) {
777
+ const score = c.EmissionScore * c.DirectionScore * c.TripDirectionScore;
778
+ if (score > 1) return 1;
779
+ if (score < 0) return 0;
780
+ return score;
781
+ }
782
+ function bearingDiffDeg(a, b) {
783
+ let d = (a - b + 540) % 360 - 180;
784
+ if (d < 0) d = -d;
785
+ return d;
786
+ }
787
+
788
+ // src/config-route.ts
789
+ var DefaultRouteMeasureRegressionToleranceMeter = 30;
790
+ var DefaultRouteClampBackwardMinConfidence = 0.55;
791
+ var DefaultRouteClampDwellSpeedKmh = 8;
792
+ var DefaultRouteMeasureAdvanceSlackMeter = 15;
793
+ var DefaultRouteSegmentSwitchHysteresisLog = 1;
794
+ var DefaultRouteSnappedJumpSlackMeter = 4;
795
+ var DefaultRouteHoldLastSegmentMaxDistM = 120;
796
+ var DefaultRouteHoldLastSegmentMaxAgeMs = 3e4;
797
+ var DefaultRouteHoldLastSegmentMinConfidence = 0.25;
798
+ var DefaultRouteWildGPSJumpMinMeter = 25;
799
+ var DefaultRouteWildGPSJumpMultiplier = 2;
800
+ var DefaultRouteWildGPSMaxAdvanceFactor = 0.5;
801
+ var DefaultRouteMaxForwardSnapMeter = 0;
802
+ var DefaultRouteNextStopPassToleranceMeter = 8;
803
+ var DefaultRouteSegmentSwitchStopRadiusMeter = 20;
804
+ var DefaultFoldedSegmentMinViable = 3;
805
+ var DefaultBranchLockSearchWindowM = 20;
806
+ var DefaultBranchUnlockNormalTicks = 3;
807
+ var DefaultFoldedSegmentMeasureSpreadM = 45;
808
+ var DefaultSnapDistanceGrowResetTicks = 2;
809
+ var DefaultSnapDistanceGrowMinDeltaM = 8;
810
+ var DefaultSnapDistanceResetMinMeter = 35;
811
+ var DefaultSnapDistanceResetMaxMeter = 100;
812
+ function RouteSnapParamsOption(p) {
813
+ return (acc) => {
814
+ Object.assign(acc, mergeRouteSnapParams(acc, p));
815
+ };
816
+ }
817
+ function WithRouteSnapParams(p) {
818
+ return RouteSnapParamsOption(p);
819
+ }
820
+ function WithPreventBackwardTransition(v) {
821
+ return (p) => {
822
+ p.PreventBackwardTransition = v;
823
+ };
824
+ }
825
+ function WithMeasureRegressionTolerance(m) {
826
+ return (p) => {
827
+ p.MeasureRegressionToleranceMeter = m;
828
+ };
829
+ }
830
+ function WithClampBackwardMinConfidence(v) {
831
+ return (p) => {
832
+ p.ClampBackwardMinConfidence = v;
833
+ };
834
+ }
835
+ function WithClampDwellSpeedKmh(kmh) {
836
+ return (p) => {
837
+ p.ClampDwellSpeedKmh = kmh;
838
+ };
839
+ }
840
+ function WithMeasureAdvanceSlack(m) {
841
+ return (p) => {
842
+ p.MeasureAdvanceSlackMeter = m;
843
+ };
844
+ }
845
+ function WithSnappedJumpSlack(m) {
846
+ return (p) => {
847
+ p.SnappedJumpSlackMeter = m;
848
+ };
849
+ }
850
+ function WithSegmentSwitchHysteresisLog(v) {
851
+ return (p) => {
852
+ p.SegmentSwitchHysteresisLog = v;
853
+ };
854
+ }
855
+ function WithLooping(v) {
856
+ return (p) => {
857
+ p.Looping = v;
858
+ };
859
+ }
860
+ function WithLoopClosureTolerance(m) {
861
+ return (p) => {
862
+ p.LoopClosureToleranceMeter = m;
863
+ };
864
+ }
865
+ function WithHoldLastSegmentOnMiss(v) {
866
+ return (p) => {
867
+ p.HoldLastSegmentOnMiss = v;
868
+ };
869
+ }
870
+ function WithHoldLastSegmentMaxDistM(m) {
871
+ return (p) => {
872
+ p.HoldLastSegmentMaxDistM = m;
873
+ };
874
+ }
875
+ function WithHoldLastSegmentMaxAgeMs(ms) {
876
+ return (p) => {
877
+ p.HoldLastSegmentMaxAgeMs = ms;
878
+ };
879
+ }
880
+ function WithHoldLastSegmentMinConfidence(v) {
881
+ return (p) => {
882
+ p.HoldLastSegmentMinConfidence = v;
883
+ };
884
+ }
885
+ function WithWildGPSStabilize(v) {
886
+ return (p) => {
887
+ p.WildGPSStabilize = v;
888
+ };
889
+ }
890
+ function WithWildGPSJumpMinMeter(m) {
891
+ return (p) => {
892
+ p.WildGPSJumpMinMeter = m;
893
+ };
894
+ }
895
+ function WithWildGPSJumpMultiplier(v) {
896
+ return (p) => {
897
+ p.WildGPSJumpMultiplier = v;
898
+ };
899
+ }
900
+ function WithWildGPSMaxAdvanceFactor(v) {
901
+ return (p) => {
902
+ p.WildGPSMaxAdvanceFactor = v;
903
+ };
904
+ }
905
+ function WithMaxForwardSnapMeter(m) {
906
+ return (p) => {
907
+ p.MaxForwardSnapMeter = m;
908
+ };
909
+ }
910
+ function WithNoBackwardSnap(v) {
911
+ return (p) => {
912
+ p.NoBackwardSnap = v;
913
+ };
914
+ }
915
+ function WithRequireNextStopBeforeSegmentSwitch(v) {
916
+ return (p) => {
917
+ p.RequireNextStopBeforeSegmentSwitch = v;
918
+ };
919
+ }
920
+ function WithNextStopPassToleranceMeter(m) {
921
+ return (p) => {
922
+ p.NextStopPassToleranceMeter = m;
923
+ };
924
+ }
925
+ function WithRequireStopRadiusForSegmentSwitch(v) {
926
+ return (p) => {
927
+ p.RequireStopRadiusForSegmentSwitch = v;
928
+ };
929
+ }
930
+ function WithSegmentSwitchStopRadiusMeter(m) {
931
+ return (p) => {
932
+ p.SegmentSwitchStopRadiusMeter = m;
933
+ };
934
+ }
935
+ function WithFoldedSegmentBranchLock(v) {
936
+ return (p) => {
937
+ p.FoldedSegmentBranchLock = v;
938
+ };
939
+ }
940
+ function WithFoldedSegmentMinViable(n) {
941
+ return (p) => {
942
+ p.FoldedSegmentMinViable = n;
943
+ };
944
+ }
945
+ function WithBranchLockSearchWindowM(m) {
946
+ return (p) => {
947
+ p.BranchLockSearchWindowM = m;
948
+ };
949
+ }
950
+ function WithBranchUnlockNormalTicks(n) {
951
+ return (p) => {
952
+ p.BranchUnlockNormalTicks = n;
953
+ };
954
+ }
955
+ function WithFoldedSegmentMeasureSpreadM(m) {
956
+ return (p) => {
957
+ p.FoldedSegmentMeasureSpreadM = m;
958
+ };
959
+ }
960
+ function WithSnapContinuityFromPrevious(v) {
961
+ return (p) => {
962
+ p.SnapContinuityFromPrevious = v;
963
+ };
964
+ }
965
+ function WithSnapDistanceResetOnGrow(v) {
966
+ return (p) => {
967
+ p.SnapDistanceResetOnGrow = v;
968
+ };
969
+ }
970
+ function WithSnapDistanceGrowResetTicks(n) {
971
+ return (p) => {
972
+ p.SnapDistanceGrowResetTicks = n;
973
+ };
974
+ }
975
+ function WithSnapDistanceGrowMinDeltaM(m) {
976
+ return (p) => {
977
+ p.SnapDistanceGrowMinDeltaM = m;
978
+ };
979
+ }
980
+ function WithSnapDistanceResetMinMeter(m) {
981
+ return (p) => {
982
+ p.SnapDistanceResetMinMeter = m;
983
+ };
984
+ }
985
+ function WithSnapDistanceResetMaxMeter(m) {
986
+ return (p) => {
987
+ p.SnapDistanceResetMaxMeter = m;
988
+ };
989
+ }
990
+ function DisableBackwardClamp() {
991
+ return (p) => {
992
+ p.ClampBackwardMinConfidence = 0;
993
+ };
994
+ }
995
+ function RouteSnapConfig(stops, ...opts) {
996
+ const params = {};
997
+ for (const opt of opts) {
998
+ if (opt) {
999
+ opt(params);
1000
+ }
1001
+ }
1002
+ return routeSnapConfig(stops, params);
1003
+ }
1004
+ function routeSnapConfig(stops, params) {
1005
+ const cfg = DefaultConfig();
1006
+ cfg.PreventBackwardTransition = params.PreventBackwardTransition ?? true;
1007
+ cfg.MeasureRegressionToleranceMeter = params.MeasureRegressionToleranceMeter ?? DefaultRouteMeasureRegressionToleranceMeter;
1008
+ cfg.ClampBackwardMinConfidence = params.ClampBackwardMinConfidence ?? DefaultRouteClampBackwardMinConfidence;
1009
+ cfg.ClampDwellSpeedKmh = params.ClampDwellSpeedKmh ?? DefaultRouteClampDwellSpeedKmh;
1010
+ cfg.MeasureAdvanceSlackMeter = params.MeasureAdvanceSlackMeter ?? DefaultRouteMeasureAdvanceSlackMeter;
1011
+ cfg.SegmentSwitchHysteresisLog = params.SegmentSwitchHysteresisLog ?? DefaultRouteSegmentSwitchHysteresisLog;
1012
+ cfg.SnappedJumpSlackMeter = params.SnappedJumpSlackMeter ?? DefaultRouteSnappedJumpSlackMeter;
1013
+ let loopTol = cfg.LoopClosureToleranceMeter;
1014
+ if (params.LoopClosureToleranceMeter != null) {
1015
+ loopTol = params.LoopClosureToleranceMeter;
1016
+ cfg.LoopClosureToleranceMeter = loopTol;
1017
+ }
1018
+ if (params.Looping != null) {
1019
+ cfg.Looping = params.Looping;
1020
+ } else {
1021
+ cfg.Looping = detectLoopFromStops(
1022
+ stops,
1023
+ cfg.AllowSameStartEndStop,
1024
+ loopTol
1025
+ );
1026
+ }
1027
+ cfg.HoldLastSegmentOnMiss = params.HoldLastSegmentOnMiss ?? true;
1028
+ cfg.HoldLastSegmentMaxDistM = params.HoldLastSegmentMaxDistM ?? DefaultRouteHoldLastSegmentMaxDistM;
1029
+ cfg.HoldLastSegmentMaxAgeMs = params.HoldLastSegmentMaxAgeMs ?? DefaultRouteHoldLastSegmentMaxAgeMs;
1030
+ cfg.HoldLastSegmentMinConfidence = params.HoldLastSegmentMinConfidence ?? DefaultRouteHoldLastSegmentMinConfidence;
1031
+ cfg.WildGPSStabilize = params.WildGPSStabilize ?? true;
1032
+ cfg.WildGPSJumpMinMeter = params.WildGPSJumpMinMeter ?? DefaultRouteWildGPSJumpMinMeter;
1033
+ cfg.WildGPSJumpMultiplier = params.WildGPSJumpMultiplier ?? DefaultRouteWildGPSJumpMultiplier;
1034
+ cfg.WildGPSMaxAdvanceFactor = params.WildGPSMaxAdvanceFactor ?? DefaultRouteWildGPSMaxAdvanceFactor;
1035
+ cfg.MaxForwardSnapMeter = params.MaxForwardSnapMeter ?? DefaultRouteMaxForwardSnapMeter;
1036
+ cfg.NoBackwardSnap = params.NoBackwardSnap ?? true;
1037
+ cfg.RequireNextStopBeforeSegmentSwitch = params.RequireNextStopBeforeSegmentSwitch ?? true;
1038
+ cfg.NextStopPassToleranceMeter = params.NextStopPassToleranceMeter ?? DefaultRouteNextStopPassToleranceMeter;
1039
+ cfg.RequireStopRadiusForSegmentSwitch = params.RequireStopRadiusForSegmentSwitch ?? true;
1040
+ cfg.SegmentSwitchStopRadiusMeter = params.SegmentSwitchStopRadiusMeter ?? DefaultRouteSegmentSwitchStopRadiusMeter;
1041
+ cfg.FoldedSegmentBranchLock = params.FoldedSegmentBranchLock ?? true;
1042
+ cfg.FoldedSegmentMinViable = params.FoldedSegmentMinViable ?? DefaultFoldedSegmentMinViable;
1043
+ cfg.BranchLockSearchWindowM = params.BranchLockSearchWindowM ?? DefaultBranchLockSearchWindowM;
1044
+ cfg.BranchUnlockNormalTicks = params.BranchUnlockNormalTicks ?? DefaultBranchUnlockNormalTicks;
1045
+ cfg.FoldedSegmentMeasureSpreadM = params.FoldedSegmentMeasureSpreadM ?? DefaultFoldedSegmentMeasureSpreadM;
1046
+ cfg.SnapContinuityFromPrevious = params.SnapContinuityFromPrevious ?? true;
1047
+ cfg.SnapDistanceResetOnGrow = params.SnapDistanceResetOnGrow ?? true;
1048
+ cfg.SnapDistanceGrowResetTicks = params.SnapDistanceGrowResetTicks ?? DefaultSnapDistanceGrowResetTicks;
1049
+ cfg.SnapDistanceGrowMinDeltaM = params.SnapDistanceGrowMinDeltaM ?? DefaultSnapDistanceGrowMinDeltaM;
1050
+ cfg.SnapDistanceResetMinMeter = params.SnapDistanceResetMinMeter ?? DefaultSnapDistanceResetMinMeter;
1051
+ cfg.SnapDistanceResetMaxMeter = params.SnapDistanceResetMaxMeter ?? DefaultSnapDistanceResetMaxMeter;
1052
+ return cfg;
1053
+ }
1054
+ function mergeRouteSnapParams(base, override) {
1055
+ const out = { ...base };
1056
+ for (const key of Object.keys(override)) {
1057
+ if (override[key] !== void 0) {
1058
+ out[key] = override[key];
1059
+ }
1060
+ }
1061
+ return out;
1062
+ }
1063
+
1064
+ // src/segment-switch.ts
1065
+ function isLoopWrapTransition(fromOrder, toOrder, segmentCount, looping) {
1066
+ if (!looping || segmentCount <= 0) {
1067
+ return false;
1068
+ }
1069
+ return fromOrder === segmentCount && toOrder === 1;
1070
+ }
1071
+ function nextStopPassTolerance(cfg) {
1072
+ let tol = cfg.NextStopPassToleranceMeter;
1073
+ if (tol <= 0) {
1074
+ tol = DefaultRouteNextStopPassToleranceMeter;
1075
+ }
1076
+ return tol;
1077
+ }
1078
+ function segmentSwitchStopRadius(cfg) {
1079
+ let r = cfg.SegmentSwitchStopRadiusMeter;
1080
+ if (r <= 0) {
1081
+ r = DefaultRouteSegmentSwitchStopRadiusMeter;
1082
+ }
1083
+ return r;
1084
+ }
1085
+ function isSegmentOrderChange(state, c) {
1086
+ if (!state?.LastBest) {
1087
+ return false;
1088
+ }
1089
+ return state.LastBest.Segment.ID !== c.Segment.ID;
1090
+ }
1091
+ function segmentSwitchGateStop(from, to) {
1092
+ if (from.ToStop.ID) {
1093
+ return [from.ToStop.Point, true];
1094
+ }
1095
+ if (to.FromStop.ID) {
1096
+ return [to.FromStop.Point, true];
1097
+ }
1098
+ return [[0, 0], false];
1099
+ }
1100
+ function forwardDepartLatchActive(state, fromOrder) {
1101
+ if (!state) {
1102
+ return false;
1103
+ }
1104
+ const latch = state.SegmentDepart;
1105
+ return latch.HasDeparted && latch.GateSegmentOrder === fromOrder;
1106
+ }
1107
+ function updateSegmentDepartLatch(state, point, cfg) {
1108
+ if (!state.LastBest || !cfg.RequireStopRadiusForSegmentSwitch) {
1109
+ return;
1110
+ }
1111
+ const from = state.LastBest.Segment;
1112
+ if (from.Order <= 0) {
1113
+ return;
1114
+ }
1115
+ let latch = { ...state.SegmentDepart };
1116
+ if (latch.GateSegmentOrder !== from.Order) {
1117
+ latch = {
1118
+ GateSegmentOrder: from.Order,
1119
+ WasInsideRadius: false,
1120
+ HasDeparted: false
1121
+ };
1122
+ }
1123
+ if (!from.ToStop.ID) {
1124
+ state.SegmentDepart = latch;
1125
+ return;
1126
+ }
1127
+ const gate = from.ToStop.Point;
1128
+ const radius = segmentSwitchStopRadius(cfg);
1129
+ const dist = DistanceMeter(point.Point, gate);
1130
+ if (dist <= radius) {
1131
+ latch.WasInsideRadius = true;
1132
+ latch.HasDeparted = false;
1133
+ } else if (latch.WasInsideRadius) {
1134
+ const departMeasure = departingSegmentMeasure(state, from, point);
1135
+ if (hasPassedSegmentDestination(departMeasure, from, cfg)) {
1136
+ latch.HasDeparted = true;
1137
+ }
1138
+ }
1139
+ state.SegmentDepart = latch;
1140
+ }
1141
+ function rejectSegmentSwitchOutsideStopRadius(state, c, segmentCount, point, cfg) {
1142
+ if (!cfg.RequireStopRadiusForSegmentSwitch || !isSegmentOrderChange(state, c)) {
1143
+ return false;
1144
+ }
1145
+ const from = state.LastBest.Segment;
1146
+ const to = c.Segment;
1147
+ const looping = cfg.Looping;
1148
+ const fromOrder = from.Order;
1149
+ const toOrder = to.Order;
1150
+ const loopWrap = isLoopWrapTransition(
1151
+ fromOrder,
1152
+ toOrder,
1153
+ segmentCount,
1154
+ looping
1155
+ );
1156
+ if (toOrder < fromOrder && !loopWrap) {
1157
+ return false;
1158
+ }
1159
+ if (toOrder > fromOrder + 1 && !loopWrap) {
1160
+ return true;
1161
+ }
1162
+ const [gate, ok] = segmentSwitchGateStop(from, to);
1163
+ if (!ok) {
1164
+ return true;
1165
+ }
1166
+ const radius = segmentSwitchStopRadius(cfg);
1167
+ if (DistanceMeter(point.Point, gate) <= radius) {
1168
+ return false;
1169
+ }
1170
+ if (toOrder === fromOrder + 1 && !loopWrap && forwardDepartLatchActive(state, fromOrder)) {
1171
+ return false;
1172
+ }
1173
+ return true;
1174
+ }
1175
+ function hasPassedSegmentDestination(measure, seg, cfg) {
1176
+ return measure >= seg.ToMeasure - nextStopPassTolerance(cfg);
1177
+ }
1178
+ function departingSegmentMeasure(state, seg, point) {
1179
+ let measure = seg.FromMeasure;
1180
+ if (state?.LastBest && state.LastBest.Segment.Order === seg.Order) {
1181
+ if (state.LastBest.Measure > measure) {
1182
+ measure = state.LastBest.Measure;
1183
+ }
1184
+ }
1185
+ const proj = ProjectPointOnLine(seg.Geometry, point.Point);
1186
+ const candidate = seg.FromMeasure + proj.Measure;
1187
+ if (candidate > measure) {
1188
+ return candidate;
1189
+ }
1190
+ return measure;
1191
+ }
1192
+ function rejectPrematureSegmentSwitch(state, c, segmentCount, point, cfg) {
1193
+ if (!cfg.RequireNextStopBeforeSegmentSwitch || !state.LastBest) {
1194
+ return false;
1195
+ }
1196
+ const from = state.LastBest.Segment;
1197
+ const to = c.Segment;
1198
+ if (from.ID === to.ID) {
1199
+ return false;
1200
+ }
1201
+ const looping = cfg.Looping;
1202
+ const fromOrder = from.Order;
1203
+ const toOrder = to.Order;
1204
+ const loopWrap = isLoopWrapTransition(
1205
+ fromOrder,
1206
+ toOrder,
1207
+ segmentCount,
1208
+ looping
1209
+ );
1210
+ if (toOrder < fromOrder && !loopWrap) {
1211
+ return false;
1212
+ }
1213
+ const departMeasure = departingSegmentMeasure(state, from, point);
1214
+ if (!hasPassedSegmentDestination(departMeasure, from, cfg)) {
1215
+ return true;
1216
+ }
1217
+ if (loopWrap) {
1218
+ return false;
1219
+ }
1220
+ if (toOrder > fromOrder + 1) {
1221
+ return true;
1222
+ }
1223
+ return false;
1224
+ }
1225
+ function rejectSegmentSwitch(state, c, segmentCount, point, cfg) {
1226
+ if (!isSegmentOrderChange(state, c)) {
1227
+ return false;
1228
+ }
1229
+ if (cfg.RequireStopRadiusForSegmentSwitch && rejectSegmentSwitchOutsideStopRadius(
1230
+ state,
1231
+ c,
1232
+ segmentCount,
1233
+ point,
1234
+ cfg
1235
+ )) {
1236
+ return true;
1237
+ }
1238
+ if (cfg.RequireNextStopBeforeSegmentSwitch && rejectPrematureSegmentSwitch(state, c, segmentCount, point, cfg)) {
1239
+ return true;
1240
+ }
1241
+ return false;
1242
+ }
1243
+
1244
+ // src/viterbi.ts
1245
+ function NewViterbiState(activeDirection) {
1246
+ return {
1247
+ LastCandidates: [],
1248
+ LastBest: null,
1249
+ LastGood: null,
1250
+ LastPoint: null,
1251
+ LastTimestamp: 0,
1252
+ ActiveDirection: activeDirection,
1253
+ BranchLock: null,
1254
+ BranchNormalTicks: 0,
1255
+ RecentBranchTicks: [],
1256
+ LastOutputSnapDistanceM: 0,
1257
+ GrowingSnapDistTicks: 0,
1258
+ SegmentDepart: {
1259
+ GateSegmentOrder: 0,
1260
+ WasInsideRadius: false,
1261
+ HasDeparted: false
1262
+ }
1263
+ };
1264
+ }
1265
+ function TransitionScore(fromOrder, toOrder, segmentCount, looping) {
1266
+ if (segmentCount === 0) {
1267
+ return 0.05;
1268
+ }
1269
+ const fromIdx = fromOrder - 1;
1270
+ const toIdx = toOrder - 1;
1271
+ if (fromIdx === toIdx) {
1272
+ return 1;
1273
+ }
1274
+ if (looping && fromIdx === segmentCount - 1 && toIdx === 0) {
1275
+ return 0.95;
1276
+ }
1277
+ const diff = toIdx - fromIdx;
1278
+ switch (true) {
1279
+ case diff === 1:
1280
+ return 0.95;
1281
+ case diff === 2:
1282
+ return 0.5;
1283
+ case diff === -1:
1284
+ return 0.15;
1285
+ case diff < 0:
1286
+ return 0.05;
1287
+ case diff > 2:
1288
+ return 0.1;
1289
+ default:
1290
+ return 0.3;
1291
+ }
1292
+ }
1293
+ function projectOntoSegment(seg, point, prev, state, cfg) {
1294
+ let prevRel = 0;
1295
+ let lastSnapped = [0, 0];
1296
+ const hasLast = state?.LastBest != null;
1297
+ if (hasLast && state.LastBest.Segment.Order === seg.Order) {
1298
+ prevRel = state.LastBest.Measure - seg.FromMeasure;
1299
+ lastSnapped = state.LastBest.SnappedPoint;
1300
+ }
1301
+ return ProjectPointOnLineContinued(
1302
+ seg.Geometry,
1303
+ point.Point,
1304
+ prevRel,
1305
+ prev,
1306
+ lastSnapped,
1307
+ cfg
1308
+ );
1309
+ }
1310
+ function findCandidates(segments, point, prev, state, cfg) {
1311
+ const raw = [];
1312
+ for (let i = 0; i < segments.length; i++) {
1313
+ const seg = segments[i];
1314
+ const proj = projectOntoSegment(seg, point, prev, state, cfg);
1315
+ if (proj.DistanceMeter > cfg.MaxSnapDistanceMeter) {
1316
+ continue;
1317
+ }
1318
+ const absMeasure = seg.FromMeasure + proj.Measure;
1319
+ raw.push({
1320
+ segmentIndex: i,
1321
+ projection: {
1322
+ Point: proj.Point,
1323
+ Measure: absMeasure,
1324
+ LineIndex: proj.LineIndex,
1325
+ DistanceMeter: proj.DistanceMeter
1326
+ }
1327
+ });
1328
+ }
1329
+ raw.sort(
1330
+ (a, b) => a.projection.DistanceMeter - b.projection.DistanceMeter
1331
+ );
1332
+ if (cfg.CandidateLimit > 0 && raw.length > cfg.CandidateLimit) {
1333
+ raw.splice(cfg.CandidateLimit);
1334
+ }
1335
+ const [busBearing, hasBearing] = resolveBusBearing(point, prev, cfg);
1336
+ const weaken = shouldWeakenDirectionValidation(point, prev, cfg);
1337
+ let activeTrip = cfg.TripDirection;
1338
+ if (!cfg.UseTripDirection) {
1339
+ activeTrip = DirectionUnknown;
1340
+ }
1341
+ const candidates = [];
1342
+ for (const item of raw) {
1343
+ const seg = segments[item.segmentIndex];
1344
+ const lineBearing = BearingAtMeasure(
1345
+ seg.Geometry,
1346
+ item.projection.Measure - seg.FromMeasure
1347
+ );
1348
+ const emission = EmissionScore(
1349
+ item.projection.DistanceMeter,
1350
+ cfg.MaxSnapDistanceMeter
1351
+ );
1352
+ const [dirScore] = scoreDirection(
1353
+ busBearing,
1354
+ hasBearing,
1355
+ lineBearing,
1356
+ cfg,
1357
+ weaken
1358
+ );
1359
+ const tripScore = TripDirectionScore(seg.Direction, activeTrip);
1360
+ candidates.push({
1361
+ SegmentIndex: item.segmentIndex,
1362
+ Segment: seg,
1363
+ Measure: item.projection.Measure,
1364
+ SnappedPoint: item.projection.Point,
1365
+ DistanceMeter: item.projection.DistanceMeter,
1366
+ LineBearing: lineBearing,
1367
+ EmissionScore: emission,
1368
+ DirectionScore: dirScore,
1369
+ TripDirectionScore: tripScore
1370
+ });
1371
+ }
1372
+ return candidates;
1373
+ }
1374
+ function rejectBackwardCandidate(state, c, segmentCount, point, cfg) {
1375
+ if (!state.LastBest || !cfg.PreventBackwardTransition) {
1376
+ return false;
1377
+ }
1378
+ const fromOrder = state.LastBest.Segment.Order;
1379
+ const toOrder = c.Segment.Order;
1380
+ const looping = cfg.Looping;
1381
+ const loopWrap = isLoopWrapTransition(
1382
+ fromOrder,
1383
+ toOrder,
1384
+ segmentCount,
1385
+ looping
1386
+ );
1387
+ if (toOrder < fromOrder && !loopWrap) {
1388
+ return true;
1389
+ }
1390
+ const tol = cfg.MeasureRegressionToleranceMeter;
1391
+ if (tol > 0 && c.Measure < state.LastBest.Measure - tol && !loopWrap) {
1392
+ return true;
1393
+ }
1394
+ const slack = cfg.MeasureAdvanceSlackMeter;
1395
+ if (slack > 0 && state.LastPoint && !loopWrap) {
1396
+ const movement = DistanceMeter(state.LastPoint.Point, point.Point);
1397
+ if (movement >= cfg.MinMovementMeter) {
1398
+ const maxAdvance = movement * 3 + slack;
1399
+ if (c.Measure > state.LastBest.Measure + maxAdvance) {
1400
+ return true;
1401
+ }
1402
+ }
1403
+ }
1404
+ const jumpSlack = cfg.SnappedJumpSlackMeter;
1405
+ if (jumpSlack > 0 && state.LastPoint && c.Segment.Order === state.LastBest.Segment.Order) {
1406
+ const jump = DistanceMeter(
1407
+ state.LastBest.SnappedPoint,
1408
+ c.SnappedPoint
1409
+ );
1410
+ let movement = DistanceMeter(state.LastPoint.Point, point.Point);
1411
+ if (movement < 1) {
1412
+ movement = 1;
1413
+ }
1414
+ if (jump > movement * 0.75 + jumpSlack) {
1415
+ return true;
1416
+ }
1417
+ }
1418
+ return false;
1419
+ }
1420
+ function viterbiTotalScore(state, c, segmentCount, cfg) {
1421
+ const emissionLog = logScore(c.EmissionScore);
1422
+ const directionLog = logScore(c.DirectionScore);
1423
+ const tripLog = logScore(c.TripDirectionScore);
1424
+ const stepLog = emissionLog + directionLog + tripLog;
1425
+ if (!state.LastBest) {
1426
+ return stepLog;
1427
+ }
1428
+ const transition = TransitionScore(
1429
+ state.LastBest.Segment.Order,
1430
+ c.Segment.Order,
1431
+ segmentCount,
1432
+ cfg.Looping
1433
+ );
1434
+ return state.LastBest.TotalLogScore + logScore(transition) + stepLog;
1435
+ }
1436
+ function applySegmentSwitchHysteresis(state, best, candidates, segmentCount, point, cfg) {
1437
+ if (!state.LastBest || !best) {
1438
+ return best;
1439
+ }
1440
+ const hyst = cfg.SegmentSwitchHysteresisLog;
1441
+ if (hyst <= 0) {
1442
+ return best;
1443
+ }
1444
+ const prevOrder = state.LastBest.Segment.Order;
1445
+ if (best.Segment.Order === prevOrder) {
1446
+ return best;
1447
+ }
1448
+ let prevBest = null;
1449
+ for (const c of candidates) {
1450
+ if (c.Segment.Order !== prevOrder) continue;
1451
+ if (rejectBackwardCandidate(state, c, segmentCount, point, cfg)) {
1452
+ continue;
1453
+ }
1454
+ const copy = { ...c };
1455
+ copy.TotalLogScore = viterbiTotalScore(
1456
+ state,
1457
+ copy,
1458
+ segmentCount,
1459
+ cfg
1460
+ );
1461
+ if (!prevBest || copy.TotalLogScore > prevBest.TotalLogScore) {
1462
+ prevBest = copy;
1463
+ }
1464
+ }
1465
+ if (!prevBest) {
1466
+ return best;
1467
+ }
1468
+ const ambiguousDistM = 12;
1469
+ if (best.DistanceMeter > ambiguousDistM || prevBest.DistanceMeter > ambiguousDistM) {
1470
+ return best;
1471
+ }
1472
+ if (Math.abs(best.DistanceMeter - prevBest.DistanceMeter) > 5) {
1473
+ return best;
1474
+ }
1475
+ if (best.TotalLogScore - prevBest.TotalLogScore < hyst) {
1476
+ return prevBest;
1477
+ }
1478
+ return best;
1479
+ }
1480
+ function pickNearestForwardCandidate(candidates, state, segmentCount, looping) {
1481
+ if (candidates.length === 0) {
1482
+ return null;
1483
+ }
1484
+ let refOrder = 0;
1485
+ if (state?.LastBest) {
1486
+ refOrder = state.LastBest.Segment.Order;
1487
+ }
1488
+ let maxOrder = refOrder + 1;
1489
+ if (refOrder <= 0) {
1490
+ maxOrder = 0;
1491
+ }
1492
+ let best = null;
1493
+ for (const c of candidates) {
1494
+ if (refOrder > 0) {
1495
+ if (c.Segment.Order < refOrder && !isLoopWrapTransition(
1496
+ refOrder,
1497
+ c.Segment.Order,
1498
+ segmentCount,
1499
+ looping
1500
+ )) {
1501
+ continue;
1502
+ }
1503
+ if (maxOrder > 0 && c.Segment.Order > maxOrder && !isLoopWrapTransition(
1504
+ refOrder,
1505
+ c.Segment.Order,
1506
+ segmentCount,
1507
+ looping
1508
+ )) {
1509
+ continue;
1510
+ }
1511
+ }
1512
+ if (!best || c.DistanceMeter < best.DistanceMeter) {
1513
+ best = { ...c };
1514
+ }
1515
+ }
1516
+ return best;
1517
+ }
1518
+ function runViterbiStep(state, candidates, segmentCount, point, cfg) {
1519
+ if (candidates.length === 0) {
1520
+ return null;
1521
+ }
1522
+ let best = null;
1523
+ const looping = cfg.Looping;
1524
+ for (const c of candidates) {
1525
+ if (rejectBackwardCandidate(state, c, segmentCount, point, cfg)) {
1526
+ continue;
1527
+ }
1528
+ if (rejectSegmentSwitch(state, c, segmentCount, point, cfg) && (cfg.RequireNextStopBeforeSegmentSwitch || cfg.RequireStopRadiusForSegmentSwitch)) {
1529
+ continue;
1530
+ }
1531
+ const emissionLog = logScore(c.EmissionScore);
1532
+ const directionLog = logScore(c.DirectionScore);
1533
+ const tripLog = logScore(c.TripDirectionScore);
1534
+ const stepLog = emissionLog + directionLog + tripLog;
1535
+ if (!state.LastBest) {
1536
+ const total2 = stepLog;
1537
+ const candidate2 = { ...c, TotalLogScore: total2 };
1538
+ if (!best || candidate2.TotalLogScore > best.TotalLogScore) {
1539
+ best = candidate2;
1540
+ }
1541
+ continue;
1542
+ }
1543
+ const transition = TransitionScore(
1544
+ state.LastBest.Segment.Order,
1545
+ c.Segment.Order,
1546
+ segmentCount,
1547
+ looping
1548
+ );
1549
+ const total = state.LastBest.TotalLogScore + logScore(transition) + stepLog;
1550
+ const candidate = { ...c, TotalLogScore: total, Prev: state.LastBest };
1551
+ if (!best || candidate.TotalLogScore > best.TotalLogScore) {
1552
+ best = candidate;
1553
+ }
1554
+ }
1555
+ best = applySegmentSwitchHysteresis(
1556
+ state,
1557
+ best,
1558
+ candidates,
1559
+ segmentCount,
1560
+ point,
1561
+ cfg
1562
+ );
1563
+ if (!best && state.LastBest && cfg.PreventBackwardTransition) {
1564
+ for (const c of candidates) {
1565
+ if (c.Segment.Order === state.LastBest.Segment.Order) {
1566
+ return { ...c };
1567
+ }
1568
+ }
1569
+ }
1570
+ if (!best && state.LastBest && cfg.PreventBackwardTransition) {
1571
+ return pickNearestForwardCandidate(
1572
+ candidates,
1573
+ state,
1574
+ segmentCount,
1575
+ looping
1576
+ );
1577
+ }
1578
+ return best;
1579
+ }
1580
+
1581
+ // src/live-bus-config.ts
1582
+ var RecommendedMaxSnapDistanceMeter = 28;
1583
+ var RecommendedMaxBearingDiffDegree = 40;
1584
+ var RecommendedMinMovementMeter = 3;
1585
+ var RecommendedMeasureRegressionToleranceMeter = 10;
1586
+ var RecommendedClampBackwardMinConfidence = 0.78;
1587
+ var RecommendedSegmentSwitchHysteresisLog = 2.5;
1588
+ var RecommendedMeasureAdvanceSlackMeter = 8;
1589
+ var RecommendedClampDwellSpeedKmh = 10;
1590
+ var RecommendedSnappedJumpSlackMeter = 2;
1591
+ var RecommendedSegmentSwitchStopRadiusMeter = 20;
1592
+ var RecommendedLoopClosureToleranceMeter = 15;
1593
+ var RecommendedLoopNextStopPassToleranceMeter = 18;
1594
+ var RecommendedSnapDistanceResetMaxMeter = 100;
1595
+ var RecommendedSnapDistanceGrowResetTicks = DefaultSnapDistanceGrowResetTicks;
1596
+ var RecommendedSnapDistanceGrowMinDeltaM = DefaultSnapDistanceGrowMinDeltaM;
1597
+ var RecommendedSnapDistanceResetMinMeter = DefaultSnapDistanceResetMinMeter;
1598
+ function IsLoopRoute(stops) {
1599
+ return detectLoopFromStops(stops, true, RecommendedLoopClosureToleranceMeter);
1600
+ }
1601
+ function LiveBusSnapConfig(stops) {
1602
+ const opts = [
1603
+ WithMeasureRegressionTolerance(
1604
+ RecommendedMeasureRegressionToleranceMeter
1605
+ ),
1606
+ WithClampBackwardMinConfidence(RecommendedClampBackwardMinConfidence),
1607
+ WithSegmentSwitchHysteresisLog(RecommendedSegmentSwitchHysteresisLog),
1608
+ WithMeasureAdvanceSlack(RecommendedMeasureAdvanceSlackMeter),
1609
+ WithClampDwellSpeedKmh(RecommendedClampDwellSpeedKmh),
1610
+ WithSnappedJumpSlack(RecommendedSnappedJumpSlackMeter),
1611
+ WithRequireStopRadiusForSegmentSwitch(true),
1612
+ WithSegmentSwitchStopRadiusMeter(RecommendedSegmentSwitchStopRadiusMeter),
1613
+ WithLoopClosureTolerance(RecommendedLoopClosureToleranceMeter),
1614
+ WithSnapDistanceResetMaxMeter(RecommendedSnapDistanceResetMaxMeter)
1615
+ ];
1616
+ if (IsLoopRoute(stops)) {
1617
+ opts.push(
1618
+ WithSnapDistanceResetOnGrow(false),
1619
+ WithNextStopPassToleranceMeter(
1620
+ RecommendedLoopNextStopPassToleranceMeter
1621
+ )
1622
+ );
1623
+ } else {
1624
+ opts.push(
1625
+ WithSnapDistanceResetOnGrow(true),
1626
+ WithSnapDistanceGrowResetTicks(RecommendedSnapDistanceGrowResetTicks),
1627
+ WithSnapDistanceGrowMinDeltaM(RecommendedSnapDistanceGrowMinDeltaM),
1628
+ WithSnapDistanceResetMinMeter(RecommendedSnapDistanceResetMinMeter)
1629
+ );
1630
+ }
1631
+ const cfg = RouteSnapConfig(stops, ...opts);
1632
+ cfg.MaxSnapDistanceMeter = RecommendedMaxSnapDistanceMeter;
1633
+ cfg.MaxBearingDiffDegree = RecommendedMaxBearingDiffDegree;
1634
+ cfg.MinMovementMeter = RecommendedMinMovementMeter;
1635
+ return cfg;
1636
+ }
1637
+
1638
+ // src/off-route-policy.ts
1639
+ var DefaultOffRouteSoftDistFraction = 0.54;
1640
+ var DefaultOffRouteMinConfidence = 0.2;
1641
+ var DefaultOffRouteSoftConfidence = 0.5;
1642
+ function DefaultOffRoutePolicy() {
1643
+ return {
1644
+ MaxSnapDistanceMeter: 28,
1645
+ OffRouteSoftDistFraction: DefaultOffRouteSoftDistFraction,
1646
+ OffRouteMinConfidence: DefaultOffRouteMinConfidence,
1647
+ OffRouteSoftConfidence: DefaultOffRouteSoftConfidence
1648
+ };
1649
+ }
1650
+ function normalized(policy) {
1651
+ const defaults = DefaultOffRoutePolicy();
1652
+ return {
1653
+ MaxSnapDistanceMeter: policy.MaxSnapDistanceMeter > 0 ? policy.MaxSnapDistanceMeter : defaults.MaxSnapDistanceMeter,
1654
+ OffRouteSoftDistFraction: policy.OffRouteSoftDistFraction > 0 ? policy.OffRouteSoftDistFraction : DefaultOffRouteSoftDistFraction,
1655
+ OffRouteMinConfidence: policy.OffRouteMinConfidence > 0 ? policy.OffRouteMinConfidence : DefaultOffRouteMinConfidence,
1656
+ OffRouteSoftConfidence: policy.OffRouteSoftConfidence > 0 ? policy.OffRouteSoftConfidence : DefaultOffRouteSoftConfidence
1657
+ };
1658
+ }
1659
+ function offRouteSoftDistM(policy) {
1660
+ const p = normalized(policy);
1661
+ return p.MaxSnapDistanceMeter * p.OffRouteSoftDistFraction;
1662
+ }
1663
+ function SnapDegraded(result) {
1664
+ if (!result) {
1665
+ return true;
1666
+ }
1667
+ if (!result.SegmentID || result.SegmentOrder <= 0) {
1668
+ return true;
1669
+ }
1670
+ if (result.RejectedReason) {
1671
+ return true;
1672
+ }
1673
+ return !!(result.HeldSegment && result.IsOffRoute);
1674
+ }
1675
+ function MapOffRoute(result, snapDegraded, policy) {
1676
+ const p = normalized(policy);
1677
+ if (!result || snapDegraded) {
1678
+ return true;
1679
+ }
1680
+ let isOffRoute = result.IsOffRoute;
1681
+ if (result.HeldSegment) {
1682
+ isOffRoute = false;
1683
+ }
1684
+ if (isOffRoute || result.HeldSegment) {
1685
+ return isOffRoute;
1686
+ }
1687
+ if (result.Confidence < p.OffRouteMinConfidence) {
1688
+ return true;
1689
+ }
1690
+ const soft = offRouteSoftDistM(p);
1691
+ if (result.DistanceMeter > soft && result.Confidence < p.OffRouteSoftConfidence) {
1692
+ return true;
1693
+ }
1694
+ return false;
1695
+ }
1696
+ function EtaSnapUnreliable(result, snapDegraded, policy) {
1697
+ const p = normalized(policy);
1698
+ if (!result || snapDegraded) {
1699
+ return true;
1700
+ }
1701
+ if (result.IsOffRoute) {
1702
+ return true;
1703
+ }
1704
+ if (result.DistanceMeter > p.MaxSnapDistanceMeter) {
1705
+ return true;
1706
+ }
1707
+ const soft = offRouteSoftDistM(p);
1708
+ if (result.DistanceMeter > soft && result.Confidence < p.OffRouteSoftConfidence) {
1709
+ return true;
1710
+ }
1711
+ if (result.Confidence < p.OffRouteMinConfidence) {
1712
+ return true;
1713
+ }
1714
+ return false;
1715
+ }
1716
+ function EtaSnapReliableForPublish(result, snapDegraded, policy) {
1717
+ if (EtaSnapUnreliable(result, snapDegraded, policy)) {
1718
+ return false;
1719
+ }
1720
+ return (result?.SegmentOrder ?? 0) > 0;
1721
+ }
1722
+
1723
+ // src/snap-helpers.ts
1724
+ var backwardMeasureEpsilonM = 0.5;
1725
+ function candidateFromProjection(ctx, seg, point, proj) {
1726
+ const absMeasure = seg.FromMeasure + proj.Measure;
1727
+ const lineBearing = BearingAtMeasure(seg.Geometry, proj.Measure);
1728
+ const [busBearing, hasBearing] = resolveBusBearing(
1729
+ point,
1730
+ ctx.state.LastPoint,
1731
+ ctx.config
1732
+ );
1733
+ const weaken = shouldWeakenDirectionValidation(
1734
+ point,
1735
+ ctx.state.LastPoint,
1736
+ ctx.config
1737
+ );
1738
+ const emission = EmissionScore(
1739
+ proj.DistanceMeter,
1740
+ ctx.config.MaxSnapDistanceMeter
1741
+ );
1742
+ const [dirScore] = scoreDirection(
1743
+ busBearing,
1744
+ hasBearing,
1745
+ lineBearing,
1746
+ ctx.config,
1747
+ weaken
1748
+ );
1749
+ const tripScore = TripDirectionScore(
1750
+ seg.Direction,
1751
+ ctx.config.TripDirection
1752
+ );
1753
+ return {
1754
+ Segment: seg,
1755
+ Measure: absMeasure,
1756
+ SnappedPoint: proj.Point,
1757
+ DistanceMeter: proj.DistanceMeter,
1758
+ LineBearing: lineBearing,
1759
+ EmissionScore: emission,
1760
+ DirectionScore: dirScore,
1761
+ TripDirectionScore: tripScore
1762
+ };
1763
+ }
1764
+ function candidateOnSegment(ctx, seg, point) {
1765
+ const proj = projectOntoSegment(
1766
+ seg,
1767
+ point,
1768
+ ctx.state.LastPoint,
1769
+ ctx.state,
1770
+ ctx.config
1771
+ );
1772
+ return candidateFromProjection(ctx, seg, point, proj);
1773
+ }
1774
+ function resultFromCandidate(ctx, best, point) {
1775
+ const [busBearing, hasBearing] = resolveBusBearing(
1776
+ point,
1777
+ ctx.state.LastPoint,
1778
+ ctx.config
1779
+ );
1780
+ const weaken = shouldWeakenDirectionValidation(
1781
+ point,
1782
+ ctx.state.LastPoint,
1783
+ ctx.config
1784
+ );
1785
+ const [, directionDiff] = scoreDirection(
1786
+ busBearing,
1787
+ hasBearing,
1788
+ best.LineBearing,
1789
+ ctx.config,
1790
+ weaken
1791
+ );
1792
+ let resolvedBusBearing = busBearing;
1793
+ if (!hasBearing && ctx.state.LastBest) {
1794
+ resolvedBusBearing = ctx.state.LastBest.LineBearing;
1795
+ }
1796
+ return {
1797
+ OriginalPoint: point.Point,
1798
+ SnappedPoint: best.SnappedPoint,
1799
+ SegmentID: best.Segment.ID,
1800
+ SegmentOrder: best.Segment.Order,
1801
+ Direction: best.Segment.Direction,
1802
+ NearestStopID: nearestStopID(ctx.stops, best.SnappedPoint),
1803
+ DistanceMeter: best.DistanceMeter,
1804
+ Progress: segmentProgress(best.Segment, best.Measure),
1805
+ BusBearing: resolvedBusBearing,
1806
+ LineBearing: best.LineBearing,
1807
+ DirectionDiff: directionDiff,
1808
+ Confidence: clampConfidence(confidenceFromScores(best)),
1809
+ IsOffRoute: best.DistanceMeter > ctx.config.MaxSnapDistanceMeter
1810
+ };
1811
+ }
1812
+ function holdReferenceCandidate(ctx) {
1813
+ if (ctx.state.LastBest?.Segment.Order) {
1814
+ return ctx.state.LastBest;
1815
+ }
1816
+ if (ctx.state.LastGood?.Segment.Order) {
1817
+ return ctx.state.LastGood;
1818
+ }
1819
+ return null;
1820
+ }
1821
+ function clampToPreviousSegmentWithMaxDist(ctx, point, maxSnapDist) {
1822
+ const ref = holdReferenceCandidate(ctx);
1823
+ if (!ref) {
1824
+ return [null, null];
1825
+ }
1826
+ const seg = ref.Segment;
1827
+ const prevRel = ref.Measure - seg.FromMeasure;
1828
+ const lastAbs = ref.Measure;
1829
+ const lastSnapped = ref.SnappedPoint;
1830
+ let tol = ctx.config.MeasureRegressionToleranceMeter;
1831
+ if (tol <= 0) {
1832
+ tol = DefaultRouteMeasureRegressionToleranceMeter;
1833
+ }
1834
+ let advanceSlack = ctx.config.MeasureAdvanceSlackMeter;
1835
+ if (advanceSlack <= 0) {
1836
+ advanceSlack = DefaultRouteMeasureAdvanceSlackMeter;
1837
+ }
1838
+ let movement = 0;
1839
+ if (ctx.state.LastPoint) {
1840
+ movement = DistanceMeter(ctx.state.LastPoint.Point, point.Point);
1841
+ }
1842
+ let maxAdvance = movement * 1.5 + advanceSlack;
1843
+ if (movement > 0 && movement < 1) {
1844
+ maxAdvance = 1.5 + advanceSlack;
1845
+ }
1846
+ let proj = ProjectPointOnLineContinued(
1847
+ seg.Geometry,
1848
+ point.Point,
1849
+ prevRel,
1850
+ ctx.state.LastPoint,
1851
+ lastSnapped,
1852
+ ctx.config
1853
+ );
1854
+ const minRel = prevRel - tol;
1855
+ const maxRel = prevRel + maxAdvance;
1856
+ const [forward, ok] = ForwardProjectionOnSegment(
1857
+ seg.Geometry,
1858
+ point.Point,
1859
+ minRel,
1860
+ maxRel,
1861
+ maxSnapDist
1862
+ );
1863
+ if (ok) {
1864
+ const absForward = seg.FromMeasure + forward.Measure;
1865
+ const absProj = seg.FromMeasure + proj.Measure;
1866
+ if (absForward >= lastAbs - tol && (absProj < lastAbs - tol || absForward > absProj)) {
1867
+ proj = forward;
1868
+ }
1869
+ }
1870
+ if (seg.FromMeasure + proj.Measure < lastAbs - tol) {
1871
+ const after = FindNearestProjectionAfterMeasure(
1872
+ seg.Geometry,
1873
+ point.Point,
1874
+ prevRel
1875
+ );
1876
+ if (after.DistanceMeter <= maxSnapDist && seg.FromMeasure + after.Measure >= lastAbs - tol) {
1877
+ proj = after;
1878
+ }
1879
+ }
1880
+ if (seg.FromMeasure + proj.Measure < lastAbs - tol) {
1881
+ proj = {
1882
+ Point: lastSnapped,
1883
+ Measure: prevRel,
1884
+ LineIndex: 0,
1885
+ DistanceMeter: DistanceMeter(point.Point, lastSnapped)
1886
+ };
1887
+ }
1888
+ const candidate = candidateFromProjection(ctx, seg, point, proj);
1889
+ const result = resultFromCandidate(ctx, candidate, point);
1890
+ return [result, candidate];
1891
+ }
1892
+ function holdLastSegmentMaxDistM(ctx) {
1893
+ if (ctx.config.HoldLastSegmentMaxDistM > 0) {
1894
+ return ctx.config.HoldLastSegmentMaxDistM;
1895
+ }
1896
+ if (ctx.config.MaxSnapDistanceMeter > 0) {
1897
+ return Math.max(
1898
+ DefaultRouteHoldLastSegmentMaxDistM,
1899
+ ctx.config.MaxSnapDistanceMeter * 2
1900
+ );
1901
+ }
1902
+ return DefaultRouteHoldLastSegmentMaxDistM;
1903
+ }
1904
+ function holdLastSegmentMaxAgeMs(ctx) {
1905
+ if (ctx.config.HoldLastSegmentMaxAgeMs > 0) {
1906
+ return ctx.config.HoldLastSegmentMaxAgeMs;
1907
+ }
1908
+ return DefaultRouteHoldLastSegmentMaxAgeMs;
1909
+ }
1910
+ function holdLastSegmentMinConfidence(ctx) {
1911
+ if (ctx.config.HoldLastSegmentMinConfidence > 0) {
1912
+ return ctx.config.HoldLastSegmentMinConfidence;
1913
+ }
1914
+ return DefaultRouteHoldLastSegmentMinConfidence;
1915
+ }
1916
+ function holdLastSegmentAgeOK(ctx, point) {
1917
+ const maxAge = holdLastSegmentMaxAgeMs(ctx);
1918
+ if (ctx.state.LastTimestamp <= 0) {
1919
+ return true;
1920
+ }
1921
+ let now = point.Timestamp ?? 0;
1922
+ if (now <= 0) {
1923
+ now = Date.now();
1924
+ }
1925
+ const delta = now - ctx.state.LastTimestamp;
1926
+ if (delta < 0) {
1927
+ return true;
1928
+ }
1929
+ return delta <= maxAge;
1930
+ }
1931
+ function applyHeldSegmentResult(ctx, result, reason) {
1932
+ const minConf = holdLastSegmentMinConfidence(ctx);
1933
+ let conf = result.Confidence * 0.5;
1934
+ if (conf < minConf) {
1935
+ conf = minConf;
1936
+ }
1937
+ return {
1938
+ ...result,
1939
+ Confidence: clampConfidence(conf),
1940
+ HeldSegment: true,
1941
+ HeldReason: reason,
1942
+ IsOffRoute: false,
1943
+ RejectedReason: ""
1944
+ };
1945
+ }
1946
+ function holdLastSegmentOnMiss(ctx, point) {
1947
+ if (!ctx.config.HoldLastSegmentOnMiss || !holdReferenceCandidate(ctx)) {
1948
+ return [null, null];
1949
+ }
1950
+ if (!holdLastSegmentAgeOK(ctx, point)) {
1951
+ return [null, null];
1952
+ }
1953
+ let projMax = holdLastSegmentMaxDistM(ctx);
1954
+ if (projMax < 500) {
1955
+ projMax = 500;
1956
+ }
1957
+ const [result, candidate] = clampToPreviousSegmentWithMaxDist(
1958
+ ctx,
1959
+ point,
1960
+ projMax
1961
+ );
1962
+ if (!result || !candidate) {
1963
+ return [null, null];
1964
+ }
1965
+ if (!result.SegmentID || result.SegmentOrder <= 0) {
1966
+ return [null, null];
1967
+ }
1968
+ return [applyHeldSegmentResult(ctx, result, "no_candidates"), candidate];
1969
+ }
1970
+ function tryHoldLastSegment(ctx, point, reason) {
1971
+ if (!ctx.config.HoldLastSegmentOnMiss) {
1972
+ return [null, null];
1973
+ }
1974
+ const [result, best] = holdLastSegmentOnMiss(ctx, point);
1975
+ if (result) {
1976
+ if (reason && reason !== "no_candidates") {
1977
+ result.HeldReason = reason;
1978
+ }
1979
+ return [result, best];
1980
+ }
1981
+ return [null, null];
1982
+ }
1983
+ function shouldClampBackward(ctx, result) {
1984
+ if (!ctx.config.PreventBackwardTransition || !ctx.state.LastBest) {
1985
+ return false;
1986
+ }
1987
+ if (isLoopWrapTransition(
1988
+ ctx.state.LastBest.Segment.Order,
1989
+ result.SegmentOrder,
1990
+ ctx.segments.length,
1991
+ ctx.config.Looping
1992
+ )) {
1993
+ return false;
1994
+ }
1995
+ return result.SegmentOrder < ctx.state.LastBest.Segment.Order;
1996
+ }
1997
+ function shouldClampMeasureRegression(ctx, result) {
1998
+ if (!ctx.state.LastBest) {
1999
+ return false;
2000
+ }
2001
+ const tol = ctx.config.MeasureRegressionToleranceMeter;
2002
+ if (tol <= 0) {
2003
+ return false;
2004
+ }
2005
+ if (isLoopWrapTransition(
2006
+ ctx.state.LastBest.Segment.Order,
2007
+ result.SegmentOrder,
2008
+ ctx.segments.length,
2009
+ ctx.config.Looping
2010
+ )) {
2011
+ return false;
2012
+ }
2013
+ const resMeasure = routeMeasure(ctx, result.SegmentOrder, result.Progress);
2014
+ return resMeasure < ctx.state.LastBest.Measure - tol;
2015
+ }
2016
+ function shouldClampLateral(ctx, result, point) {
2017
+ if (!ctx.state.LastBest || !ctx.state.LastPoint) {
2018
+ return false;
2019
+ }
2020
+ if (ctx.config.ClampBackwardMinConfidence <= 0) {
2021
+ return false;
2022
+ }
2023
+ const jump = DistanceMeter(
2024
+ ctx.state.LastBest.SnappedPoint,
2025
+ result.SnappedPoint
2026
+ );
2027
+ let movement = DistanceMeter(ctx.state.LastPoint.Point, point.Point);
2028
+ if (movement < 1) {
2029
+ movement = 1;
2030
+ }
2031
+ let jumpSlack = ctx.config.SnappedJumpSlackMeter;
2032
+ if (jumpSlack <= 0) {
2033
+ jumpSlack = DefaultRouteSnappedJumpSlackMeter;
2034
+ }
2035
+ let dwellSpeed = ctx.config.ClampDwellSpeedKmh;
2036
+ if (dwellSpeed <= 0) {
2037
+ dwellSpeed = DefaultRouteClampDwellSpeedKmh;
2038
+ }
2039
+ const isDwell = (point.Speed ?? 0) < 3 || ctx.config.UseSpeed && (point.Speed ?? 0) <= dwellSpeed || movement < ctx.config.MinMovementMeter;
2040
+ let confLimit = 0.7;
2041
+ if (isDwell) {
2042
+ confLimit = 0.85;
2043
+ }
2044
+ if (result.Confidence >= confLimit) {
2045
+ return false;
2046
+ }
2047
+ if (isDwell && jump > jumpSlack) {
2048
+ return true;
2049
+ }
2050
+ return jump > movement * 0.75 + jumpSlack && jump > 4;
2051
+ }
2052
+ function stabilizeSameSegmentCandidate(ctx, best, point) {
2053
+ if (!ctx.state.LastBest || !best || best.Segment.Order !== ctx.state.LastBest.Segment.Order) {
2054
+ return best;
2055
+ }
2056
+ let jumpSlack = ctx.config.SnappedJumpSlackMeter;
2057
+ if (jumpSlack <= 0) {
2058
+ jumpSlack = DefaultRouteSnappedJumpSlackMeter;
2059
+ }
2060
+ let movement = 1;
2061
+ if (ctx.state.LastPoint) {
2062
+ movement = DistanceMeter(ctx.state.LastPoint.Point, point.Point);
2063
+ if (movement < 1) {
2064
+ movement = 1;
2065
+ }
2066
+ }
2067
+ const jump = DistanceMeter(
2068
+ ctx.state.LastBest.SnappedPoint,
2069
+ best.SnappedPoint
2070
+ );
2071
+ const maxJump = movement * 1.25 + jumpSlack;
2072
+ if (jump <= maxJump || jump <= 5) {
2073
+ return best;
2074
+ }
2075
+ const continued = candidateOnSegment(
2076
+ ctx,
2077
+ ctx.state.LastBest.Segment,
2078
+ point
2079
+ );
2080
+ let tol = ctx.config.MeasureRegressionToleranceMeter;
2081
+ if (tol <= 0) {
2082
+ tol = DefaultRouteMeasureRegressionToleranceMeter;
2083
+ }
2084
+ if (continued.Measure >= ctx.state.LastBest.Measure - tol) {
2085
+ return continued;
2086
+ }
2087
+ return best;
2088
+ }
2089
+ function softMaxSnapDist(ctx) {
2090
+ if (ctx.config.MaxSnapDistanceMeter <= 0) {
2091
+ return 18;
2092
+ }
2093
+ const soft = ctx.config.MaxSnapDistanceMeter * 0.65;
2094
+ return soft < 12 ? 12 : soft;
2095
+ }
2096
+ function preferNearbyOnActiveSegment(ctx, best, point) {
2097
+ if (!ctx.state.LastBest || !best) {
2098
+ return best;
2099
+ }
2100
+ if (forwardDepartLatchActive(ctx.state, ctx.state.LastBest.Segment.Order)) {
2101
+ return best;
2102
+ }
2103
+ const soft = softMaxSnapDist(ctx);
2104
+ if (best.DistanceMeter <= soft) {
2105
+ return best;
2106
+ }
2107
+ const lastSeg = ctx.state.LastBest.Segment;
2108
+ const onLast = candidateOnSegment(ctx, lastSeg, point);
2109
+ if (onLast.DistanceMeter > ctx.config.MaxSnapDistanceMeter) {
2110
+ return best;
2111
+ }
2112
+ let tol = ctx.config.MeasureRegressionToleranceMeter;
2113
+ if (tol <= 0) {
2114
+ tol = DefaultRouteMeasureRegressionToleranceMeter;
2115
+ }
2116
+ const measureOK = onLast.Measure >= ctx.state.LastBest.Measure - tol;
2117
+ if (best.Segment.Order === lastSeg.Order && onLast.DistanceMeter < best.DistanceMeter) {
2118
+ return onLast;
2119
+ }
2120
+ if (onLast.DistanceMeter + 2 < best.DistanceMeter && measureOK) {
2121
+ return onLast;
2122
+ }
2123
+ if (best.Segment.Order !== lastSeg.Order && best.DistanceMeter > soft && onLast.DistanceMeter <= soft && measureOK) {
2124
+ return onLast;
2125
+ }
2126
+ return best;
2127
+ }
2128
+ function segmentAtRouteMeasure(ctx, m) {
2129
+ for (const seg of ctx.segments) {
2130
+ if (m >= seg.FromMeasure && m <= seg.ToMeasure) {
2131
+ return seg;
2132
+ }
2133
+ }
2134
+ if (ctx.segments.length === 0) {
2135
+ return null;
2136
+ }
2137
+ const last = ctx.segments[ctx.segments.length - 1];
2138
+ if (m > last.ToMeasure) {
2139
+ return last;
2140
+ }
2141
+ const first = ctx.segments[0];
2142
+ if (m < first.FromMeasure) {
2143
+ return first;
2144
+ }
2145
+ return null;
2146
+ }
2147
+ function candidateAtRouteMeasure(ctx, seg, absMeasure, point) {
2148
+ let rel = absMeasure - seg.FromMeasure;
2149
+ if (rel < 0) rel = 0;
2150
+ const segLen = seg.ToMeasure - seg.FromMeasure;
2151
+ if (segLen > 0 && rel > segLen) {
2152
+ rel = segLen;
2153
+ }
2154
+ const [pt] = PointAtMeasure(seg.Geometry, rel);
2155
+ const proj = {
2156
+ Point: pt,
2157
+ Measure: rel,
2158
+ DistanceMeter: DistanceMeter(point.Point, pt)};
2159
+ const candidate = candidateFromProjection(ctx, seg, point, proj);
2160
+ candidate.Measure = seg.FromMeasure + rel;
2161
+ const result = resultFromCandidate(ctx, candidate, point);
2162
+ return [result, candidate];
2163
+ }
2164
+ function routeMeasure(ctx, order, progress) {
2165
+ for (const seg of ctx.segments) {
2166
+ if (seg.Order === order) {
2167
+ return seg.FromMeasure + progress * (seg.ToMeasure - seg.FromMeasure);
2168
+ }
2169
+ }
2170
+ return 0;
2171
+ }
2172
+ function gpsMovesForwardAlongRoute(ctx, point, ref) {
2173
+ if (!ctx.state.LastPoint || !ref) {
2174
+ return false;
2175
+ }
2176
+ const movement = DistanceMeter(ctx.state.LastPoint.Point, point.Point);
2177
+ let minMov = ctx.config.MinMovementMeter;
2178
+ if (minMov <= 0) {
2179
+ minMov = 3;
2180
+ }
2181
+ if (movement < minMov) {
2182
+ return false;
2183
+ }
2184
+ const moveBearing = BearingBetween(
2185
+ ctx.state.LastPoint.Point,
2186
+ point.Point
2187
+ );
2188
+ return bearingDiffDeg(moveBearing, ref.LineBearing) <= 90;
2189
+ }
2190
+ function applyStabilizedResult(ctx, result, reason) {
2191
+ const minConf = holdLastSegmentMinConfidence(ctx);
2192
+ let conf = result.Confidence * 0.6;
2193
+ if (conf < minConf) {
2194
+ conf = minConf;
2195
+ }
2196
+ return {
2197
+ ...result,
2198
+ Confidence: clampConfidence(conf),
2199
+ HeldSegment: true,
2200
+ HeldReason: reason,
2201
+ IsOffRoute: false,
2202
+ RejectedReason: ""
2203
+ };
2204
+ }
2205
+ function freezeAtRefCandidate(ctx, point, ref, reason) {
2206
+ if (!ref) {
2207
+ return [null, null];
2208
+ }
2209
+ const seg = ref.Segment;
2210
+ const prevRel = ref.Measure - seg.FromMeasure;
2211
+ const proj = {
2212
+ Point: ref.SnappedPoint,
2213
+ Measure: prevRel,
2214
+ DistanceMeter: DistanceMeter(point.Point, ref.SnappedPoint)};
2215
+ const candidate = candidateFromProjection(ctx, seg, point, proj);
2216
+ candidate.Measure = ref.Measure;
2217
+ let result = resultFromCandidate(ctx, candidate, point);
2218
+ result = applyStabilizedResult(ctx, result, reason);
2219
+ return [result, candidate];
2220
+ }
2221
+ function plausibleRawGPSMovementM(ctx, point) {
2222
+ let slack = ctx.config.MeasureAdvanceSlackMeter;
2223
+ if (slack <= 0) {
2224
+ slack = DefaultRouteMeasureAdvanceSlackMeter;
2225
+ }
2226
+ let deltaSec = 1;
2227
+ if ((point.Timestamp ?? 0) > 0 && ctx.state.LastTimestamp > 0) {
2228
+ deltaSec = (point.Timestamp - ctx.state.LastTimestamp) / 1e3;
2229
+ }
2230
+ if (deltaSec < 0.3) deltaSec = 0.3;
2231
+ if (deltaSec > 30) deltaSec = 30;
2232
+ let speedKmh = point.Speed ?? 0;
2233
+ if (speedKmh <= 0 && ctx.state.LastPoint) {
2234
+ speedKmh = ctx.state.LastPoint.Speed ?? 0;
2235
+ }
2236
+ if (speedKmh <= 0) {
2237
+ speedKmh = 40;
2238
+ }
2239
+ return speedKmh / 3.6 * deltaSec + slack;
2240
+ }
2241
+ function isWildGPSJump(ctx, point) {
2242
+ if (!ctx.state.LastPoint) {
2243
+ return false;
2244
+ }
2245
+ const raw = DistanceMeter(ctx.state.LastPoint.Point, point.Point);
2246
+ let minM = ctx.config.WildGPSJumpMinMeter;
2247
+ if (minM <= 0) {
2248
+ minM = DefaultRouteWildGPSJumpMinMeter;
2249
+ }
2250
+ if (raw < minM) {
2251
+ return false;
2252
+ }
2253
+ let mult = ctx.config.WildGPSJumpMultiplier;
2254
+ if (mult <= 0) {
2255
+ mult = DefaultRouteWildGPSJumpMultiplier;
2256
+ }
2257
+ return raw > plausibleRawGPSMovementM(ctx, point) * mult;
2258
+ }
2259
+ function creepForwardOnSegment(ctx, point, ref) {
2260
+ if (!ref) {
2261
+ return [null, null];
2262
+ }
2263
+ let creep = plausibleRawGPSMovementM(ctx, point);
2264
+ if (ctx.config.MaxForwardSnapMeter > 0 && creep > ctx.config.MaxForwardSnapMeter) {
2265
+ creep = ctx.config.MaxForwardSnapMeter;
2266
+ }
2267
+ let advSlack = ctx.config.MeasureAdvanceSlackMeter;
2268
+ if (advSlack <= 0) {
2269
+ advSlack = DefaultRouteMeasureAdvanceSlackMeter;
2270
+ }
2271
+ if (ctx.state.LastPoint) {
2272
+ const movement = DistanceMeter(
2273
+ ctx.state.LastPoint.Point,
2274
+ point.Point
2275
+ );
2276
+ const cap = movement * 1.5 + advSlack;
2277
+ if (creep > cap) {
2278
+ creep = cap;
2279
+ }
2280
+ }
2281
+ if (creep < 0.5) {
2282
+ return [null, null];
2283
+ }
2284
+ let targetM = ref.Measure + creep;
2285
+ let seg = segmentAtRouteMeasure(ctx, targetM);
2286
+ if (!seg || seg.Order < ref.Segment.Order) {
2287
+ seg = ref.Segment;
2288
+ targetM = Math.min(ref.Measure + creep, ref.Segment.ToMeasure);
2289
+ }
2290
+ return candidateAtRouteMeasure(ctx, seg, targetM, point);
2291
+ }
2292
+ function advanceAlongRoute(ctx, point, reason) {
2293
+ const ref = ctx.state.LastBest;
2294
+ if (!ref) {
2295
+ return [null, null];
2296
+ }
2297
+ let maxDist = holdLastSegmentMaxDistM(ctx);
2298
+ if (maxDist < 500) {
2299
+ maxDist = 500;
2300
+ }
2301
+ const [result, candidate] = clampToPreviousSegmentWithMaxDist(
2302
+ ctx,
2303
+ point,
2304
+ maxDist
2305
+ );
2306
+ if (candidate && candidate.Measure > ref.Measure + backwardMeasureEpsilonM) {
2307
+ if (result) {
2308
+ return [applyStabilizedResult(ctx, result, reason), candidate];
2309
+ }
2310
+ }
2311
+ if (gpsMovesForwardAlongRoute(ctx, point, ref)) {
2312
+ const [crept, c] = creepForwardOnSegment(ctx, point, ref);
2313
+ if (crept) {
2314
+ return [applyStabilizedResult(ctx, crept, reason), c];
2315
+ }
2316
+ }
2317
+ return freezeAtRefCandidate(ctx, point, ref, reason);
2318
+ }
2319
+ function noBackwardSnapEnabled(ctx) {
2320
+ if (ctx.config.NoBackwardSnap) {
2321
+ return true;
2322
+ }
2323
+ return ctx.config.PreventBackwardTransition;
2324
+ }
2325
+ function enforceNoBackwardSnap(ctx, best, point) {
2326
+ if (!noBackwardSnapEnabled(ctx) || !ctx.state.LastBest || !best) {
2327
+ return [null, null];
2328
+ }
2329
+ const ref = ctx.state.LastBest;
2330
+ const loopWrap = isLoopWrapTransition(
2331
+ ref.Segment.Order,
2332
+ best.Segment.Order,
2333
+ ctx.segments.length,
2334
+ ctx.config.Looping
2335
+ );
2336
+ if (loopWrap) {
2337
+ return [null, null];
2338
+ }
2339
+ let backward = false;
2340
+ if (best.Segment.Order < ref.Segment.Order) {
2341
+ backward = true;
2342
+ }
2343
+ if (best.Measure < ref.Measure - backwardMeasureEpsilonM) {
2344
+ backward = true;
2345
+ }
2346
+ if (!backward) {
2347
+ return [null, null];
2348
+ }
2349
+ if (best.Measure < ref.Measure - backwardMeasureEpsilonM && !gpsMovesForwardAlongRoute(ctx, point, ref)) {
2350
+ return freezeAtRefCandidate(ctx, point, ref, "no_backward");
2351
+ }
2352
+ return advanceAlongRoute(ctx, point, "no_backward");
2353
+ }
2354
+ function snapDistanceResetEnabled(cfg) {
2355
+ return cfg.SnapDistanceResetOnGrow;
2356
+ }
2357
+ function snapDistanceGrowResetTicks(cfg) {
2358
+ return cfg.SnapDistanceGrowResetTicks > 0 ? cfg.SnapDistanceGrowResetTicks : DefaultSnapDistanceGrowResetTicks;
2359
+ }
2360
+ function snapDistanceGrowMinDeltaM(cfg) {
2361
+ return cfg.SnapDistanceGrowMinDeltaM > 0 ? cfg.SnapDistanceGrowMinDeltaM : DefaultSnapDistanceGrowMinDeltaM;
2362
+ }
2363
+ function snapDistanceResetMinMeter(cfg) {
2364
+ if (cfg.SnapDistanceResetMinMeter > 0) {
2365
+ return cfg.SnapDistanceResetMinMeter;
2366
+ }
2367
+ const maxSnap = cfg.MaxSnapDistanceMeter;
2368
+ if (maxSnap > 0) {
2369
+ return maxSnap + 4;
2370
+ }
2371
+ return DefaultSnapDistanceResetMinMeter;
2372
+ }
2373
+ function snapDistanceResetMaxMeter(cfg) {
2374
+ if (cfg.SnapDistanceResetMaxMeter === 0) {
2375
+ return 0;
2376
+ }
2377
+ if (cfg.SnapDistanceResetMaxMeter > 0) {
2378
+ return cfg.SnapDistanceResetMaxMeter;
2379
+ }
2380
+ return DefaultSnapDistanceResetMaxMeter;
2381
+ }
2382
+ function lateralDistFromLastSnapM(ctx, point) {
2383
+ if (!ctx.state.LastBest) {
2384
+ return 0;
2385
+ }
2386
+ const proj = projectOntoSegment(
2387
+ ctx.state.LastBest.Segment,
2388
+ point,
2389
+ ctx.state.LastPoint,
2390
+ ctx.state,
2391
+ ctx.config
2392
+ );
2393
+ return proj.DistanceMeter;
2394
+ }
2395
+ function resetSnapTrackingState(ctx) {
2396
+ const activeDir = ctx.state.ActiveDirection;
2397
+ ctx.state.LastCandidates = [];
2398
+ ctx.state.LastBest = null;
2399
+ ctx.state.LastGood = null;
2400
+ ctx.state.LastPoint = null;
2401
+ ctx.state.LastTimestamp = 0;
2402
+ ctx.state.BranchLock = null;
2403
+ ctx.state.BranchNormalTicks = 0;
2404
+ ctx.state.RecentBranchTicks = [];
2405
+ ctx.state.LastOutputSnapDistanceM = 0;
2406
+ ctx.state.GrowingSnapDistTicks = 0;
2407
+ ctx.state.SegmentDepart = {
2408
+ GateSegmentOrder: 0,
2409
+ WasInsideRadius: false,
2410
+ HasDeparted: false
2411
+ };
2412
+ ctx.state.ActiveDirection = activeDir;
2413
+ }
2414
+ function maybeResetSnapDistance(ctx, point) {
2415
+ if (!ctx.state.LastBest) {
2416
+ return false;
2417
+ }
2418
+ const ref = ctx.state.LastBest;
2419
+ const lateralDist = lateralDistFromLastSnapM(ctx, point);
2420
+ const maxReset = snapDistanceResetMaxMeter(ctx.config);
2421
+ if (maxReset > 0 && lateralDist >= maxReset) {
2422
+ resetSnapTrackingState(ctx);
2423
+ return true;
2424
+ }
2425
+ if (!snapDistanceResetEnabled(ctx.config)) {
2426
+ return false;
2427
+ }
2428
+ const dist = DistanceMeter(point.Point, ref.SnappedPoint);
2429
+ const minDist = snapDistanceResetMinMeter(ctx.config);
2430
+ if (dist < minDist) {
2431
+ ctx.state.GrowingSnapDistTicks = 0;
2432
+ return false;
2433
+ }
2434
+ let maxSnap = ctx.config.MaxSnapDistanceMeter;
2435
+ if (maxSnap <= 0) {
2436
+ maxSnap = 28;
2437
+ }
2438
+ if (ctx.state.LastPoint && dist < maxSnap * 2) {
2439
+ const moveBearing = BearingBetween(
2440
+ ctx.state.LastPoint.Point,
2441
+ point.Point
2442
+ );
2443
+ if (bearingDiffDeg(moveBearing, ref.LineBearing) > 90) {
2444
+ ctx.state.GrowingSnapDistTicks = 0;
2445
+ return false;
2446
+ }
2447
+ }
2448
+ const prevDist = ctx.state.LastOutputSnapDistanceM;
2449
+ const growDelta = snapDistanceGrowMinDeltaM(ctx.config);
2450
+ if (prevDist > 0 && dist > prevDist + growDelta) {
2451
+ ctx.state.GrowingSnapDistTicks++;
2452
+ } else if (dist <= prevDist) {
2453
+ ctx.state.GrowingSnapDistTicks = 0;
2454
+ }
2455
+ if (ctx.state.GrowingSnapDistTicks < snapDistanceGrowResetTicks(ctx.config)) {
2456
+ return false;
2457
+ }
2458
+ resetSnapTrackingState(ctx);
2459
+ return true;
2460
+ }
2461
+ function markDistanceReset(result, distReset) {
2462
+ if (distReset && result && !result.HeldReason) {
2463
+ return { ...result, HeldReason: "snap_distance_reset" };
2464
+ }
2465
+ return result;
2466
+ }
2467
+ function commitSnapState(ctx, candidates, best, point, result) {
2468
+ ctx.state.LastCandidates = candidates;
2469
+ ctx.state.LastBest = best;
2470
+ if (best && best.Segment.Order > 0) {
2471
+ ctx.state.LastGood = { ...best };
2472
+ }
2473
+ ctx.state.LastPoint = point;
2474
+ let ts = point.Timestamp ?? 0;
2475
+ if (ts <= 0) {
2476
+ ts = Date.now();
2477
+ }
2478
+ ctx.state.LastTimestamp = ts;
2479
+ if (result) {
2480
+ ctx.state.LastOutputSnapDistanceM = result.DistanceMeter;
2481
+ }
2482
+ }
2483
+ function finishSnap(ctx, candidates, best, result, point) {
2484
+ if (!best) {
2485
+ return result;
2486
+ }
2487
+ let out = result ?? resultFromCandidate(ctx, best, point);
2488
+ const [adjusted, fBest] = enforceNoBackwardSnap(ctx, best, point);
2489
+ if (adjusted) {
2490
+ best = fBest;
2491
+ out = adjusted;
2492
+ }
2493
+ commitSnapState(ctx, candidates, best, point, out);
2494
+ return out;
2495
+ }
2496
+ function isBackwardSegmentTransition(ctx, best) {
2497
+ if (!best || !ctx.state.LastBest || !ctx.config.PreventBackwardTransition) {
2498
+ return false;
2499
+ }
2500
+ const ref = ctx.state.LastBest;
2501
+ if (best.Segment.Order >= ref.Segment.Order) {
2502
+ return false;
2503
+ }
2504
+ return !isLoopWrapTransition(
2505
+ ref.Segment.Order,
2506
+ best.Segment.Order,
2507
+ ctx.segments.length,
2508
+ ctx.config.Looping
2509
+ );
2510
+ }
2511
+ function enforceForwardSegmentOrder(ctx, best, point) {
2512
+ if (!isBackwardSegmentTransition(ctx, best)) {
2513
+ return best;
2514
+ }
2515
+ return candidateOnSegment(ctx, ctx.state.LastBest.Segment, point);
2516
+ }
2517
+ function capForwardSnapAdvance(ctx, best, point) {
2518
+ const maxFwd = ctx.config.MaxForwardSnapMeter;
2519
+ if (maxFwd <= 0 || !ctx.state.LastBest || !best) {
2520
+ return [null, null];
2521
+ }
2522
+ const ref = ctx.state.LastBest;
2523
+ const loopWrap = isLoopWrapTransition(
2524
+ ref.Segment.Order,
2525
+ best.Segment.Order,
2526
+ ctx.segments.length,
2527
+ ctx.config.Looping
2528
+ );
2529
+ if (loopWrap) {
2530
+ return [null, null];
2531
+ }
2532
+ const lastM = ref.Measure;
2533
+ const newM = best.Measure;
2534
+ if (newM <= lastM + maxFwd) {
2535
+ return [null, null];
2536
+ }
2537
+ const targetM = lastM + maxFwd;
2538
+ const seg = segmentAtRouteMeasure(ctx, targetM);
2539
+ if (!seg) {
2540
+ return [null, null];
2541
+ }
2542
+ const [result, candidate] = candidateAtRouteMeasure(
2543
+ ctx,
2544
+ seg,
2545
+ targetM,
2546
+ point
2547
+ );
2548
+ if (!result) {
2549
+ return [null, null];
2550
+ }
2551
+ const minConf = holdLastSegmentMinConfidence(ctx);
2552
+ if (result.Confidence < minConf) {
2553
+ result.Confidence = minConf;
2554
+ }
2555
+ result.HeldSegment = true;
2556
+ result.HeldReason = "forward_cap";
2557
+ result.IsOffRoute = false;
2558
+ return [result, candidate];
2559
+ }
2560
+ function stabilizeWildGPSJump(ctx, best, point) {
2561
+ if (!ctx.config.WildGPSStabilize || !ctx.state.LastBest || !best || !ctx.state.LastPoint) {
2562
+ return [null, null];
2563
+ }
2564
+ if (!isWildGPSJump(ctx, point)) {
2565
+ return [null, null];
2566
+ }
2567
+ const ref = ctx.state.LastBest;
2568
+ let tol = ctx.config.MeasureRegressionToleranceMeter;
2569
+ if (tol <= 0) {
2570
+ tol = DefaultRouteMeasureRegressionToleranceMeter;
2571
+ }
2572
+ const loopWrap = isLoopWrapTransition(
2573
+ ref.Segment.Order,
2574
+ best.Segment.Order,
2575
+ ctx.segments.length,
2576
+ ctx.config.Looping
2577
+ );
2578
+ const lastM = ref.Measure;
2579
+ const newM = best.Measure;
2580
+ if (best.Segment.Order < ref.Segment.Order && !loopWrap) {
2581
+ return advanceAlongRoute(ctx, point, "wild_gps_backward");
2582
+ }
2583
+ if (newM < lastM - tol && !loopWrap) {
2584
+ if (gpsMovesForwardAlongRoute(ctx, point, ref)) {
2585
+ return advanceAlongRoute(ctx, point, "wild_gps_regression");
2586
+ }
2587
+ return freezeAtRefCandidate(ctx, point, ref, "wild_gps_regression");
2588
+ }
2589
+ return [null, null];
2590
+ }
2591
+ function maxAllowedMeasureAdvanceM(cfg, gpsMovement) {
2592
+ const maxFwd = cfg.MaxForwardSnapMeter;
2593
+ if (maxFwd <= 0) {
2594
+ let advanceSlack = cfg.MeasureAdvanceSlackMeter;
2595
+ if (advanceSlack <= 0) {
2596
+ advanceSlack = DefaultRouteMeasureAdvanceSlackMeter;
2597
+ }
2598
+ let mov = gpsMovement;
2599
+ if (mov < 1) mov = 1;
2600
+ return mov * 3 + advanceSlack;
2601
+ }
2602
+ return maxFwd;
2603
+ }
2604
+ function resnapOnActiveSegmentWhenDrifted(ctx, best, point) {
2605
+ if (!ctx.state.LastBest) {
2606
+ return best;
2607
+ }
2608
+ const ref = ctx.state.LastBest;
2609
+ let maxSnap = ctx.config.MaxSnapDistanceMeter;
2610
+ if (maxSnap <= 0) maxSnap = 28;
2611
+ const dist = DistanceMeter(point.Point, ref.SnappedPoint);
2612
+ const minDrift = Math.max(maxSnap * 1.15, 20);
2613
+ if (dist < minDrift) {
2614
+ return best;
2615
+ }
2616
+ if (isWildGPSJump(ctx, point)) {
2617
+ return best;
2618
+ }
2619
+ let maxDist = holdLastSegmentMaxDistM(ctx);
2620
+ if (maxDist < maxSnap * 2) {
2621
+ maxDist = maxSnap * 2;
2622
+ }
2623
+ const [, candidate] = clampToPreviousSegmentWithMaxDist(
2624
+ ctx,
2625
+ point,
2626
+ maxDist
2627
+ );
2628
+ if (!candidate) {
2629
+ return best;
2630
+ }
2631
+ let tol = ctx.config.MeasureRegressionToleranceMeter;
2632
+ if (tol <= 0) {
2633
+ tol = DefaultRouteMeasureRegressionToleranceMeter;
2634
+ }
2635
+ if (candidate.Measure < ref.Measure - tol) {
2636
+ return best;
2637
+ }
2638
+ let gpsMovement = 0;
2639
+ if (ctx.state.LastPoint) {
2640
+ gpsMovement = DistanceMeter(
2641
+ ctx.state.LastPoint.Point,
2642
+ point.Point
2643
+ );
2644
+ }
2645
+ const maxAdv = maxAllowedMeasureAdvanceM(ctx.config, gpsMovement);
2646
+ if (candidate.Measure > ref.Measure + maxAdv) {
2647
+ return best;
2648
+ }
2649
+ if (best) {
2650
+ const bestRaw = DistanceMeter(point.Point, best.SnappedPoint);
2651
+ const candRaw = DistanceMeter(point.Point, candidate.SnappedPoint);
2652
+ if (candRaw >= bestRaw) {
2653
+ return best;
2654
+ }
2655
+ }
2656
+ return candidate;
2657
+ }
2658
+ function applySnapContinuityResult(ctx, best, point, result) {
2659
+ if (result?.HeldReason === "forward_cap") {
2660
+ return [null, best];
2661
+ }
2662
+ if (!ctx.config.SnapContinuityFromPrevious || !ctx.state.LastBest || !best || !ctx.state.LastPoint) {
2663
+ return [null, best];
2664
+ }
2665
+ const ref = ctx.state.LastBest;
2666
+ if (best.Segment.Order !== ref.Segment.Order) {
2667
+ return [null, best];
2668
+ }
2669
+ const measureAdv = best.Measure - ref.Measure;
2670
+ const snapJump = DistanceMeter(ref.SnappedPoint, best.SnappedPoint);
2671
+ if (measureAdv > backwardMeasureEpsilonM || snapJump > backwardMeasureEpsilonM) {
2672
+ return [null, best];
2673
+ }
2674
+ let gpsMovement = DistanceMeter(
2675
+ ctx.state.LastPoint.Point,
2676
+ point.Point
2677
+ );
2678
+ let minMov = ctx.config.MinMovementMeter;
2679
+ if (minMov <= 0) minMov = 3;
2680
+ if (gpsMovement < minMov) {
2681
+ return [null, best];
2682
+ }
2683
+ if (!gpsMovesForwardAlongRoute(ctx, point, ref)) {
2684
+ return [null, best];
2685
+ }
2686
+ const [crept, c] = creepForwardOnSegment(ctx, point, ref);
2687
+ if (!crept || !c || c.Measure <= ref.Measure + backwardMeasureEpsilonM) {
2688
+ return [null, best];
2689
+ }
2690
+ const contResult = resultFromCandidate(ctx, c, point);
2691
+ contResult.HeldSegment = true;
2692
+ contResult.HeldReason = "snap_continuity_creep";
2693
+ const minConf = holdLastSegmentMinConfidence(ctx);
2694
+ if (contResult.Confidence < minConf) {
2695
+ contResult.Confidence = minConf;
2696
+ }
2697
+ contResult.IsOffRoute = contResult.DistanceMeter > ctx.config.MaxSnapDistanceMeter;
2698
+ return [contResult, c];
2699
+ }
2700
+ function ensureForwardCreepWhenStuck(ctx, best, point) {
2701
+ return applySnapContinuityResult(ctx, best, point, null);
2702
+ }
2703
+
2704
+ // src/snapper.ts
2705
+ var Snapper = class _Snapper {
2706
+ line;
2707
+ segments;
2708
+ stops;
2709
+ config;
2710
+ state;
2711
+ constructor(line, stops, cfg) {
2712
+ if (line.length < 2) {
2713
+ throw ErrInvalidLine;
2714
+ }
2715
+ if (stops.length < 2) {
2716
+ throw ErrInsufficientStops;
2717
+ }
2718
+ const sortedStops = sortStopsByOrder(stops);
2719
+ const segments = BuildSegments(line, sortedStops, cfg);
2720
+ if (segments.length === 0) {
2721
+ throw ErrEmptySegments;
2722
+ }
2723
+ let activeDirection = DirectionUnknown;
2724
+ if (cfg.UseTripDirection) {
2725
+ activeDirection = cfg.TripDirection;
2726
+ }
2727
+ this.line = line;
2728
+ this.segments = segments;
2729
+ this.stops = sortedStops;
2730
+ this.config = cfg;
2731
+ this.state = NewViterbiState(activeDirection);
2732
+ }
2733
+ static NewSnapper(line, stops, cfg) {
2734
+ return new _Snapper(line, stops, cfg);
2735
+ }
2736
+ ctx() {
2737
+ return {
2738
+ line: this.line,
2739
+ segments: this.segments,
2740
+ stops: this.stops,
2741
+ config: this.config,
2742
+ state: this.state
2743
+ };
2744
+ }
2745
+ Snap(point) {
2746
+ const ctx = this.ctx();
2747
+ const distReset = maybeResetSnapDistance(ctx, point);
2748
+ updateSegmentDepartLatch(this.state, point, this.config);
2749
+ const candidates = findCandidates(
2750
+ this.segments,
2751
+ point,
2752
+ this.state.LastPoint,
2753
+ this.state,
2754
+ this.config
2755
+ );
2756
+ if (candidates.length === 0) {
2757
+ const [held, heldBest] = tryHoldLastSegment(ctx, point, "no_candidates");
2758
+ if (held) {
2759
+ let best2 = heldBest;
2760
+ const promoted2 = this.enforceDepartLatch(heldBest, point);
2761
+ if (promoted2) {
2762
+ best2 = promoted2;
2763
+ const updated = resultFromCandidate(ctx, promoted2, point);
2764
+ return markDistanceReset(
2765
+ finishSnap(ctx, [], best2, updated, point),
2766
+ distReset
2767
+ );
2768
+ }
2769
+ return markDistanceReset(
2770
+ finishSnap(ctx, [], best2, held, point),
2771
+ distReset
2772
+ );
2773
+ }
2774
+ return markDistanceReset(
2775
+ {
2776
+ OriginalPoint: point.Point,
2777
+ SnappedPoint: point.Point,
2778
+ SegmentID: "",
2779
+ SegmentOrder: 0,
2780
+ Direction: DirectionUnknown,
2781
+ NearestStopID: "",
2782
+ DistanceMeter: 0,
2783
+ Progress: 0,
2784
+ BusBearing: 0,
2785
+ LineBearing: 0,
2786
+ DirectionDiff: 0,
2787
+ Confidence: 0,
2788
+ IsOffRoute: true,
2789
+ RejectedReason: "no candidates within max snap distance"
2790
+ },
2791
+ distReset
2792
+ );
2793
+ }
2794
+ let best = runViterbiStep(
2795
+ this.state,
2796
+ candidates,
2797
+ this.segments.length,
2798
+ point,
2799
+ this.config
2800
+ );
2801
+ if (!best) {
2802
+ if (this.state.LastBest && this.config.PreventBackwardTransition) {
2803
+ best = candidateOnSegment(
2804
+ ctx,
2805
+ this.state.LastBest.Segment,
2806
+ point
2807
+ );
2808
+ } else {
2809
+ throw ErrNoCandidates;
2810
+ }
2811
+ }
2812
+ best = stabilizeSameSegmentCandidate(ctx, best, point) ?? best;
2813
+ best = preferNearbyOnActiveSegment(ctx, best, point) ?? best;
2814
+ const enforced = this.enforceNextStopBeforeSegmentSwitch(best, point);
2815
+ if (enforced) {
2816
+ best = enforced;
2817
+ }
2818
+ const promoted = this.enforceDepartLatch(best, point);
2819
+ if (promoted) {
2820
+ best = promoted;
2821
+ }
2822
+ if (this.state.LastBest && this.config.PreventBackwardTransition) {
2823
+ const prevOrder = this.state.LastBest.Segment.Order;
2824
+ if (best.Segment.Order < prevOrder && !isLoopWrapTransition(
2825
+ prevOrder,
2826
+ best.Segment.Order,
2827
+ this.segments.length,
2828
+ this.config.Looping
2829
+ )) {
2830
+ best = candidateOnSegment(ctx, this.state.LastBest.Segment, point);
2831
+ }
2832
+ }
2833
+ let result = resultFromCandidate(ctx, best, point);
2834
+ if (shouldClampBackward(ctx, result) || shouldClampMeasureRegression(ctx, result) || shouldClampLateral(ctx, result, point)) {
2835
+ const [clamped] = clampToPreviousSegmentWithMaxDist(
2836
+ ctx,
2837
+ point,
2838
+ this.config.MaxSnapDistanceMeter
2839
+ );
2840
+ if (clamped && this.state.LastBest) {
2841
+ result = clamped;
2842
+ best = candidateOnSegment(ctx, this.state.LastBest.Segment, point);
2843
+ }
2844
+ }
2845
+ if (!result.SegmentID || result.SegmentOrder <= 0) {
2846
+ const [held, heldBest] = tryHoldLastSegment(ctx, point, "empty_segment");
2847
+ if (held) {
2848
+ return markDistanceReset(
2849
+ finishSnap(ctx, candidates, heldBest, held, point),
2850
+ distReset
2851
+ );
2852
+ }
2853
+ }
2854
+ const [wildResult, wildBest] = stabilizeWildGPSJump(ctx, best, point);
2855
+ if (wildResult) {
2856
+ best = wildBest;
2857
+ result = wildResult;
2858
+ }
2859
+ const [capped, capBest] = capForwardSnapAdvance(ctx, best, point);
2860
+ if (capped) {
2861
+ best = capBest;
2862
+ result = capped;
2863
+ }
2864
+ const [contResult, contBest] = applySnapContinuityResult(
2865
+ ctx,
2866
+ best,
2867
+ point,
2868
+ result
2869
+ );
2870
+ if (contResult) {
2871
+ best = contBest;
2872
+ result = contResult;
2873
+ }
2874
+ const [creepResult, creepBest] = ensureForwardCreepWhenStuck(
2875
+ ctx,
2876
+ best,
2877
+ point
2878
+ );
2879
+ if (creepResult) {
2880
+ best = creepBest;
2881
+ result = creepResult;
2882
+ }
2883
+ const resnapped = resnapOnActiveSegmentWhenDrifted(ctx, best, point);
2884
+ if (resnapped && resnapped !== best) {
2885
+ best = resnapped;
2886
+ result = resultFromCandidate(ctx, best, point);
2887
+ }
2888
+ best = enforceForwardSegmentOrder(ctx, best, point) ?? best;
2889
+ if (result && this.state.LastBest && (result.SegmentOrder < this.state.LastBest.Segment.Order || best && best.Segment.Order < this.state.LastBest.Segment.Order && !isLoopWrapTransition(
2890
+ this.state.LastBest.Segment.Order,
2891
+ best.Segment.Order,
2892
+ this.segments.length,
2893
+ this.config.Looping
2894
+ ))) {
2895
+ result = resultFromCandidate(ctx, best, point);
2896
+ }
2897
+ result = finishSnap(ctx, candidates, best, result, point);
2898
+ return markDistanceReset(result, distReset);
2899
+ }
2900
+ enforceNextStopBeforeSegmentSwitch(best, point) {
2901
+ if (!best || !this.state.LastBest) {
2902
+ return null;
2903
+ }
2904
+ if (!this.config.RequireNextStopBeforeSegmentSwitch && !this.config.RequireStopRadiusForSegmentSwitch) {
2905
+ return null;
2906
+ }
2907
+ if (rejectSegmentSwitch(
2908
+ this.state,
2909
+ best,
2910
+ this.segments.length,
2911
+ point,
2912
+ this.config
2913
+ )) {
2914
+ return candidateOnSegment(this.ctx(), this.state.LastBest.Segment, point);
2915
+ }
2916
+ return null;
2917
+ }
2918
+ enforceDepartLatch(best, point) {
2919
+ if (!this.config.RequireStopRadiusForSegmentSwitch || !this.state.LastBest) {
2920
+ return null;
2921
+ }
2922
+ const fromOrder = this.state.LastBest.Segment.Order;
2923
+ if (!forwardDepartLatchActive(this.state, fromOrder)) {
2924
+ return null;
2925
+ }
2926
+ const nextOrder = fromOrder + 1;
2927
+ if (best && best.Segment.Order >= nextOrder) {
2928
+ return null;
2929
+ }
2930
+ const [nextSeg, ok] = segmentWithOrder(this.segments, nextOrder);
2931
+ if (!ok) {
2932
+ return null;
2933
+ }
2934
+ return candidateOnSegment(this.ctx(), nextSeg, point);
2935
+ }
2936
+ Reset() {
2937
+ this.state.LastCandidates = [];
2938
+ this.state.LastBest = null;
2939
+ this.state.LastGood = null;
2940
+ this.state.LastPoint = null;
2941
+ this.state.LastTimestamp = 0;
2942
+ this.state.BranchLock = null;
2943
+ this.state.BranchNormalTicks = 0;
2944
+ this.state.RecentBranchTicks = [];
2945
+ this.state.LastOutputSnapDistanceM = 0;
2946
+ this.state.GrowingSnapDistTicks = 0;
2947
+ this.state.SegmentDepart = {
2948
+ GateSegmentOrder: 0,
2949
+ WasInsideRadius: false,
2950
+ HasDeparted: false
2951
+ };
2952
+ }
2953
+ SetTripDirection(direction) {
2954
+ this.config.TripDirection = direction;
2955
+ this.config.UseTripDirection = direction !== DirectionUnknown;
2956
+ this.state.ActiveDirection = direction;
2957
+ }
2958
+ Segments() {
2959
+ return this.segments.map((s) => ({ ...s, Geometry: [...s.Geometry] }));
2960
+ }
2961
+ Config() {
2962
+ return { ...this.config };
2963
+ }
2964
+ RouteMeasure(order, progress) {
2965
+ return routeMeasure(this.ctx(), order, progress);
2966
+ }
2967
+ PointAtRouteMeasure(order, progress) {
2968
+ const m = this.RouteMeasure(order, progress);
2969
+ const [p] = PointAtMeasure(this.line, m);
2970
+ return p;
2971
+ }
2972
+ };
2973
+ function NewSnapper(line, stops, cfg) {
2974
+ return Snapper.NewSnapper(line, stops, cfg);
2975
+ }
2976
+ function SnapResultFromSegment(seg, stops, point, prev, cfg) {
2977
+ const proj = ProjectPointOnLine(seg.Geometry, point.Point);
2978
+ const absMeasure = seg.FromMeasure + proj.Measure;
2979
+ const lineBearing = BearingAtMeasure(seg.Geometry, proj.Measure);
2980
+ const [busBearing, hasBearing] = resolveBusBearing(point, prev, cfg);
2981
+ const weaken = shouldWeakenDirectionValidation(point, prev, cfg);
2982
+ const [, directionDiff] = scoreDirection(
2983
+ busBearing,
2984
+ hasBearing,
2985
+ lineBearing,
2986
+ cfg,
2987
+ weaken
2988
+ );
2989
+ const resolvedBusBearing = hasBearing ? busBearing : lineBearing;
2990
+ const emission = EmissionScore(proj.DistanceMeter, cfg.MaxSnapDistanceMeter);
2991
+ const [dirScore] = scoreDirection(
2992
+ busBearing,
2993
+ hasBearing,
2994
+ lineBearing,
2995
+ cfg,
2996
+ weaken
2997
+ );
2998
+ const tripScore = TripDirectionScore(seg.Direction, cfg.TripDirection);
2999
+ const confidence = clampConfidence(emission * dirScore * tripScore);
3000
+ return {
3001
+ OriginalPoint: point.Point,
3002
+ SnappedPoint: proj.Point,
3003
+ SegmentID: seg.ID,
3004
+ SegmentOrder: seg.Order,
3005
+ Direction: seg.Direction,
3006
+ NearestStopID: nearestStopID(stops, proj.Point),
3007
+ DistanceMeter: proj.DistanceMeter,
3008
+ Progress: segmentProgress(seg, absMeasure),
3009
+ BusBearing: resolvedBusBearing,
3010
+ LineBearing: lineBearing,
3011
+ DirectionDiff: directionDiff,
3012
+ Confidence: confidence,
3013
+ IsOffRoute: proj.DistanceMeter > cfg.MaxSnapDistanceMeter
3014
+ };
3015
+ }
3016
+
3017
+ exports.BearingAtMeasure = BearingAtMeasure;
3018
+ exports.BearingBetween = BearingBetween;
3019
+ exports.BearingDiff = BearingDiff;
3020
+ exports.BuildSegments = BuildSegments;
3021
+ exports.BuildSegmentsFromProjectedStops = BuildSegmentsFromProjectedStops;
3022
+ exports.CountOccurrence = CountOccurrence;
3023
+ exports.DefaultBranchLockSearchWindowM = DefaultBranchLockSearchWindowM;
3024
+ exports.DefaultBranchUnlockNormalTicks = DefaultBranchUnlockNormalTicks;
3025
+ exports.DefaultConfig = DefaultConfig;
3026
+ exports.DefaultFoldedSegmentMeasureSpreadM = DefaultFoldedSegmentMeasureSpreadM;
3027
+ exports.DefaultFoldedSegmentMinViable = DefaultFoldedSegmentMinViable;
3028
+ exports.DefaultOffRouteMinConfidence = DefaultOffRouteMinConfidence;
3029
+ exports.DefaultOffRoutePolicy = DefaultOffRoutePolicy;
3030
+ exports.DefaultOffRouteSoftConfidence = DefaultOffRouteSoftConfidence;
3031
+ exports.DefaultOffRouteSoftDistFraction = DefaultOffRouteSoftDistFraction;
3032
+ exports.DefaultRouteClampBackwardMinConfidence = DefaultRouteClampBackwardMinConfidence;
3033
+ exports.DefaultRouteClampDwellSpeedKmh = DefaultRouteClampDwellSpeedKmh;
3034
+ exports.DefaultRouteHoldLastSegmentMaxAgeMs = DefaultRouteHoldLastSegmentMaxAgeMs;
3035
+ exports.DefaultRouteHoldLastSegmentMaxDistM = DefaultRouteHoldLastSegmentMaxDistM;
3036
+ exports.DefaultRouteHoldLastSegmentMinConfidence = DefaultRouteHoldLastSegmentMinConfidence;
3037
+ exports.DefaultRouteMaxForwardSnapMeter = DefaultRouteMaxForwardSnapMeter;
3038
+ exports.DefaultRouteMeasureAdvanceSlackMeter = DefaultRouteMeasureAdvanceSlackMeter;
3039
+ exports.DefaultRouteMeasureRegressionToleranceMeter = DefaultRouteMeasureRegressionToleranceMeter;
3040
+ exports.DefaultRouteNextStopPassToleranceMeter = DefaultRouteNextStopPassToleranceMeter;
3041
+ exports.DefaultRouteSegmentSwitchHysteresisLog = DefaultRouteSegmentSwitchHysteresisLog;
3042
+ exports.DefaultRouteSegmentSwitchStopRadiusMeter = DefaultRouteSegmentSwitchStopRadiusMeter;
3043
+ exports.DefaultRouteSnappedJumpSlackMeter = DefaultRouteSnappedJumpSlackMeter;
3044
+ exports.DefaultRouteWildGPSJumpMinMeter = DefaultRouteWildGPSJumpMinMeter;
3045
+ exports.DefaultRouteWildGPSJumpMultiplier = DefaultRouteWildGPSJumpMultiplier;
3046
+ exports.DefaultRouteWildGPSMaxAdvanceFactor = DefaultRouteWildGPSMaxAdvanceFactor;
3047
+ exports.DefaultSnapDistanceGrowMinDeltaM = DefaultSnapDistanceGrowMinDeltaM;
3048
+ exports.DefaultSnapDistanceGrowResetTicks = DefaultSnapDistanceGrowResetTicks;
3049
+ exports.DefaultSnapDistanceResetMaxMeter = DefaultSnapDistanceResetMaxMeter;
3050
+ exports.DefaultSnapDistanceResetMinMeter = DefaultSnapDistanceResetMinMeter;
3051
+ exports.DetectLoopClosure = DetectLoopClosure;
3052
+ exports.DirectionInbound = DirectionInbound;
3053
+ exports.DirectionLoop = DirectionLoop;
3054
+ exports.DirectionOutbound = DirectionOutbound;
3055
+ exports.DirectionScore = DirectionScore;
3056
+ exports.DirectionUnknown = DirectionUnknown;
3057
+ exports.DisableBackwardClamp = DisableBackwardClamp;
3058
+ exports.DistanceMeter = DistanceMeter;
3059
+ exports.EmissionScore = EmissionScore;
3060
+ exports.ErrEmptySegments = ErrEmptySegments;
3061
+ exports.ErrInsufficientStops = ErrInsufficientStops;
3062
+ exports.ErrInvalidLine = ErrInvalidLine;
3063
+ exports.ErrInvalidMeasureRange = ErrInvalidMeasureRange;
3064
+ exports.ErrNoCandidates = ErrNoCandidates;
3065
+ exports.ErrStopMeasureNotMonotonic = ErrStopMeasureNotMonotonic;
3066
+ exports.EtaSnapReliableForPublish = EtaSnapReliableForPublish;
3067
+ exports.EtaSnapUnreliable = EtaSnapUnreliable;
3068
+ exports.FindNearestProjectionAfterMeasure = FindNearestProjectionAfterMeasure;
3069
+ exports.FindProjectionCandidates = FindProjectionCandidates;
3070
+ exports.FindProjectionNearMeasure = FindProjectionNearMeasure;
3071
+ exports.ForwardProjectionOnSegment = ForwardProjectionOnSegment;
3072
+ exports.IsLoopRoute = IsLoopRoute;
3073
+ exports.IsSameStop = IsSameStop;
3074
+ exports.LineLengthMeter = LineLengthMeter;
3075
+ exports.LiveBusSnapConfig = LiveBusSnapConfig;
3076
+ exports.MapOffRoute = MapOffRoute;
3077
+ exports.NewSnapper = NewSnapper;
3078
+ exports.NewViterbiState = NewViterbiState;
3079
+ exports.PointAtMeasure = PointAtMeasure;
3080
+ exports.ProjectPointOnLine = ProjectPointOnLine;
3081
+ exports.ProjectPointOnLineContinued = ProjectPointOnLineContinued;
3082
+ exports.ProjectStopForwardOnly = ProjectStopForwardOnly;
3083
+ exports.ProjectStopNearMeasure = ProjectStopNearMeasure;
3084
+ exports.ProjectStopsSequential = ProjectStopsSequential;
3085
+ exports.RecommendedClampBackwardMinConfidence = RecommendedClampBackwardMinConfidence;
3086
+ exports.RecommendedClampDwellSpeedKmh = RecommendedClampDwellSpeedKmh;
3087
+ exports.RecommendedLoopClosureToleranceMeter = RecommendedLoopClosureToleranceMeter;
3088
+ exports.RecommendedLoopNextStopPassToleranceMeter = RecommendedLoopNextStopPassToleranceMeter;
3089
+ exports.RecommendedMaxBearingDiffDegree = RecommendedMaxBearingDiffDegree;
3090
+ exports.RecommendedMaxSnapDistanceMeter = RecommendedMaxSnapDistanceMeter;
3091
+ exports.RecommendedMeasureAdvanceSlackMeter = RecommendedMeasureAdvanceSlackMeter;
3092
+ exports.RecommendedMeasureRegressionToleranceMeter = RecommendedMeasureRegressionToleranceMeter;
3093
+ exports.RecommendedMinMovementMeter = RecommendedMinMovementMeter;
3094
+ exports.RecommendedSegmentSwitchHysteresisLog = RecommendedSegmentSwitchHysteresisLog;
3095
+ exports.RecommendedSegmentSwitchStopRadiusMeter = RecommendedSegmentSwitchStopRadiusMeter;
3096
+ exports.RecommendedSnapDistanceGrowMinDeltaM = RecommendedSnapDistanceGrowMinDeltaM;
3097
+ exports.RecommendedSnapDistanceGrowResetTicks = RecommendedSnapDistanceGrowResetTicks;
3098
+ exports.RecommendedSnapDistanceResetMaxMeter = RecommendedSnapDistanceResetMaxMeter;
3099
+ exports.RecommendedSnapDistanceResetMinMeter = RecommendedSnapDistanceResetMinMeter;
3100
+ exports.RecommendedSnappedJumpSlackMeter = RecommendedSnappedJumpSlackMeter;
3101
+ exports.RouteSnapConfig = RouteSnapConfig;
3102
+ exports.RouteSnapParamsOption = RouteSnapParamsOption;
3103
+ exports.SliceLineByMeasure = SliceLineByMeasure;
3104
+ exports.SnapDegraded = SnapDegraded;
3105
+ exports.SnapResultFromSegment = SnapResultFromSegment;
3106
+ exports.SnapToLineError = SnapToLineError;
3107
+ exports.Snapper = Snapper;
3108
+ exports.TransitionScore = TransitionScore;
3109
+ exports.TripDirectionScore = TripDirectionScore;
3110
+ exports.WithBranchLockSearchWindowM = WithBranchLockSearchWindowM;
3111
+ exports.WithBranchUnlockNormalTicks = WithBranchUnlockNormalTicks;
3112
+ exports.WithClampBackwardMinConfidence = WithClampBackwardMinConfidence;
3113
+ exports.WithClampDwellSpeedKmh = WithClampDwellSpeedKmh;
3114
+ exports.WithFoldedSegmentBranchLock = WithFoldedSegmentBranchLock;
3115
+ exports.WithFoldedSegmentMeasureSpreadM = WithFoldedSegmentMeasureSpreadM;
3116
+ exports.WithFoldedSegmentMinViable = WithFoldedSegmentMinViable;
3117
+ exports.WithHoldLastSegmentMaxAgeMs = WithHoldLastSegmentMaxAgeMs;
3118
+ exports.WithHoldLastSegmentMaxDistM = WithHoldLastSegmentMaxDistM;
3119
+ exports.WithHoldLastSegmentMinConfidence = WithHoldLastSegmentMinConfidence;
3120
+ exports.WithHoldLastSegmentOnMiss = WithHoldLastSegmentOnMiss;
3121
+ exports.WithLoopClosureTolerance = WithLoopClosureTolerance;
3122
+ exports.WithLooping = WithLooping;
3123
+ exports.WithMaxForwardSnapMeter = WithMaxForwardSnapMeter;
3124
+ exports.WithMeasureAdvanceSlack = WithMeasureAdvanceSlack;
3125
+ exports.WithMeasureRegressionTolerance = WithMeasureRegressionTolerance;
3126
+ exports.WithNextStopPassToleranceMeter = WithNextStopPassToleranceMeter;
3127
+ exports.WithNoBackwardSnap = WithNoBackwardSnap;
3128
+ exports.WithPreventBackwardTransition = WithPreventBackwardTransition;
3129
+ exports.WithRequireNextStopBeforeSegmentSwitch = WithRequireNextStopBeforeSegmentSwitch;
3130
+ exports.WithRequireStopRadiusForSegmentSwitch = WithRequireStopRadiusForSegmentSwitch;
3131
+ exports.WithRouteSnapParams = WithRouteSnapParams;
3132
+ exports.WithSegmentSwitchHysteresisLog = WithSegmentSwitchHysteresisLog;
3133
+ exports.WithSegmentSwitchStopRadiusMeter = WithSegmentSwitchStopRadiusMeter;
3134
+ exports.WithSnapContinuityFromPrevious = WithSnapContinuityFromPrevious;
3135
+ exports.WithSnapDistanceGrowMinDeltaM = WithSnapDistanceGrowMinDeltaM;
3136
+ exports.WithSnapDistanceGrowResetTicks = WithSnapDistanceGrowResetTicks;
3137
+ exports.WithSnapDistanceResetMaxMeter = WithSnapDistanceResetMaxMeter;
3138
+ exports.WithSnapDistanceResetMinMeter = WithSnapDistanceResetMinMeter;
3139
+ exports.WithSnapDistanceResetOnGrow = WithSnapDistanceResetOnGrow;
3140
+ exports.WithSnappedJumpSlack = WithSnappedJumpSlack;
3141
+ exports.WithWildGPSJumpMinMeter = WithWildGPSJumpMinMeter;
3142
+ exports.WithWildGPSJumpMultiplier = WithWildGPSJumpMultiplier;
3143
+ exports.WithWildGPSMaxAdvanceFactor = WithWildGPSMaxAdvanceFactor;
3144
+ exports.WithWildGPSStabilize = WithWildGPSStabilize;
3145
+ exports.clampConfidence = clampConfidence;
3146
+ exports.confidenceFromScores = confidenceFromScores;
3147
+ exports.detectLoopFromStops = detectLoopFromStops;
3148
+ exports.findCandidates = findCandidates;
3149
+ exports.forwardDepartLatchActive = forwardDepartLatchActive;
3150
+ exports.hasPassedSegmentDestination = hasPassedSegmentDestination;
3151
+ exports.isLoopWrapTransition = isLoopWrapTransition;
3152
+ exports.logScore = logScore;
3153
+ exports.nearestStopID = nearestStopID;
3154
+ exports.pickNearestForwardCandidate = pickNearestForwardCandidate;
3155
+ exports.projectOntoSegment = projectOntoSegment;
3156
+ exports.rejectBackwardCandidate = rejectBackwardCandidate;
3157
+ exports.rejectSegmentSwitch = rejectSegmentSwitch;
3158
+ exports.runViterbiStep = runViterbiStep;
3159
+ exports.segmentProgress = segmentProgress;
3160
+ exports.segmentWithOrder = segmentWithOrder;
3161
+ exports.sortStopsByOrder = sortStopsByOrder;
3162
+ exports.updateSegmentDepartLatch = updateSegmentDepartLatch;
3163
+ //# sourceMappingURL=index.cjs.map
3164
+ //# sourceMappingURL=index.cjs.map