@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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.cjs +3164 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +385 -0
- package/dist/index.d.ts +385 -0
- package/dist/index.js +3017 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
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
|