@mapslibvn/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -95,6 +95,7 @@ function poiSourceClause(arrayExpr) {
95
95
  }
96
96
 
97
97
  // src/client.ts
98
+ var latLng = ([lat, lng]) => `${lat},${lng}`;
98
99
  function createClient(options) {
99
100
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
100
101
  const poiSources = normalizePoiSources(options.poiSources ?? DEFAULT_POI_SOURCES);
@@ -177,6 +178,15 @@ function createClient(options) {
177
178
  limit: opts.limit
178
179
  }),
179
180
  reverse: (lat, lng) => get("/v1/reverse", { lat, lng, sources }),
181
+ /** Chỉ đường (spec dẫn đường A). Response dùng [lng, lat]; tham số vào dùng [lat, lng]. */
182
+ directions: (opts) => get("/v1/directions", {
183
+ from: latLng(opts.from),
184
+ to: latLng(opts.to),
185
+ via: opts.via && opts.via.length > 0 ? opts.via.map(latLng).join(";") : void 0,
186
+ mode: opts.mode,
187
+ lang: opts.lang,
188
+ alternatives: opts.alternatives === void 0 ? void 0 : opts.alternatives ? 1 : 0
189
+ }),
180
190
  /** Gửi đóng góp/sửa POI (spec 6.1). Khoá phải có scope edits:write. */
181
191
  suggestEdit: (edit) => post("/v1/edits", edit)
182
192
  };
@@ -594,6 +604,10 @@ function adminAliasKeys(input) {
594
604
 
595
605
  // src/style-transform.ts
596
606
  var POI_LAYER_ID = "poi";
607
+ var FIRST_SYMBOL_LAYER_ID = {
608
+ light: "road_one_way_arrow",
609
+ dark: "water_name"
610
+ };
597
611
  var SOVEREIGNTY_LABEL_ID = "sovereignty-label";
598
612
  function isPoiStyleLayer(layer) {
599
613
  return layer.id === POI_LAYER_ID || layer.source === "poi";
@@ -792,39 +806,872 @@ function looksLikeTelex(normalized) {
792
806
  function foldTelex(normalized) {
793
807
  return normalized.replace(/aa/g, "a").replace(/ee/g, "e").replace(/oo/g, "o").replace(/dd/g, "d").replace(/aw/g, "a").replace(/ow/g, "o").replace(/uw/g, "u").replace(/([aeiouy](?:ch|ng|nh|[cmnpt])?)[sfrxj]\b/g, "$1").replace(/([a-z])[1-9]\b/g, "$1").replace(/\s+/g, " ").trim();
794
808
  }
809
+
810
+ // src/polyline.ts
811
+ function decodePolyline6(encoded) {
812
+ const coords = [];
813
+ let index = 0;
814
+ let lat = 0;
815
+ let lng = 0;
816
+ const next = () => {
817
+ let result = 0;
818
+ let shift = 0;
819
+ let byte;
820
+ do {
821
+ byte = encoded.charCodeAt(index++) - 63;
822
+ result |= (byte & 31) << shift;
823
+ shift += 5;
824
+ } while (byte >= 32);
825
+ return result & 1 ? ~(result >> 1) : result >> 1;
826
+ };
827
+ while (index < encoded.length) {
828
+ lat += next();
829
+ lng += next();
830
+ coords.push([lng / 1e6, lat / 1e6]);
831
+ }
832
+ return coords;
833
+ }
834
+ function encodePolyline6(coords) {
835
+ let out = "";
836
+ let prevLat = 0;
837
+ let prevLng = 0;
838
+ for (const [lng, lat] of coords) {
839
+ const iLat = Math.round(lat * 1e6);
840
+ const iLng = Math.round(lng * 1e6);
841
+ out += encodeValue(iLat - prevLat) + encodeValue(iLng - prevLng);
842
+ prevLat = iLat;
843
+ prevLng = iLng;
844
+ }
845
+ return out;
846
+ }
847
+ function encodeValue(value) {
848
+ let v = value < 0 ? ~(value << 1) : value << 1;
849
+ let out = "";
850
+ while (v >= 32) {
851
+ out += String.fromCharCode((32 | v & 31) + 63);
852
+ v >>= 5;
853
+ }
854
+ return out + String.fromCharCode(v + 63);
855
+ }
856
+
857
+ // src/maneuver.ts
858
+ var MANEUVER_KINDS = [
859
+ "depart",
860
+ "arrive",
861
+ "continue",
862
+ "slight_right",
863
+ "slight_left",
864
+ "turn_right",
865
+ "turn_left",
866
+ "sharp_right",
867
+ "sharp_left",
868
+ "uturn_right",
869
+ "uturn_left",
870
+ "ramp_straight",
871
+ "ramp_right",
872
+ "ramp_left",
873
+ "exit_right",
874
+ "exit_left",
875
+ "keep_right",
876
+ "keep_left",
877
+ "merge",
878
+ "merge_right",
879
+ "merge_left",
880
+ "roundabout_enter",
881
+ "roundabout_exit",
882
+ "ferry_enter",
883
+ "ferry_exit",
884
+ "elevator",
885
+ "steps",
886
+ "escalator",
887
+ "building_enter",
888
+ "building_exit",
889
+ "other"
890
+ ];
891
+ var VALHALLA_MANEUVER_KIND = {
892
+ 1: "depart",
893
+ 2: "depart",
894
+ 3: "depart",
895
+ 4: "arrive",
896
+ 5: "arrive",
897
+ 6: "arrive",
898
+ 7: "continue",
899
+ 8: "continue",
900
+ 9: "slight_right",
901
+ 10: "turn_right",
902
+ 11: "sharp_right",
903
+ 12: "uturn_right",
904
+ 13: "uturn_left",
905
+ 14: "sharp_left",
906
+ 15: "turn_left",
907
+ 16: "slight_left",
908
+ 17: "ramp_straight",
909
+ 18: "ramp_right",
910
+ 19: "ramp_left",
911
+ 20: "exit_right",
912
+ 21: "exit_left",
913
+ 22: "continue",
914
+ 23: "keep_right",
915
+ 24: "keep_left",
916
+ 25: "merge",
917
+ 26: "roundabout_enter",
918
+ 27: "roundabout_exit",
919
+ 28: "ferry_enter",
920
+ 29: "ferry_exit",
921
+ 37: "merge_right",
922
+ 38: "merge_left",
923
+ 39: "elevator",
924
+ 40: "steps",
925
+ 41: "escalator",
926
+ 42: "building_enter",
927
+ 43: "building_exit"
928
+ };
929
+ function maneuverKindFromValhalla(type) {
930
+ return VALHALLA_MANEUVER_KIND[type] ?? "other";
931
+ }
932
+
933
+ // src/navigation/types.ts
934
+ var NAVIGATION_THRESHOLDS = {
935
+ walk: {
936
+ offRoute_m: 25,
937
+ offRouteFixes: 3,
938
+ offRouteSeconds: 5,
939
+ maxAccuracy_m: 60,
940
+ approach_m: 40,
941
+ pre_m: 15,
942
+ arrive_m: 15,
943
+ rerouteCooldown_s: 15,
944
+ rerouteMaxFailures: 3
945
+ },
946
+ motorbike: {
947
+ offRoute_m: 40,
948
+ offRouteFixes: 3,
949
+ offRouteSeconds: 5,
950
+ maxAccuracy_m: 100,
951
+ approach_m: 200,
952
+ pre_m: 50,
953
+ arrive_m: 25,
954
+ rerouteCooldown_s: 15,
955
+ rerouteMaxFailures: 3
956
+ },
957
+ car: {
958
+ offRoute_m: 50,
959
+ offRouteFixes: 3,
960
+ offRouteSeconds: 5,
961
+ maxAccuracy_m: 100,
962
+ approach_m: 400,
963
+ pre_m: 80,
964
+ arrive_m: 30,
965
+ rerouteCooldown_s: 15,
966
+ rerouteMaxFailures: 3
967
+ }
968
+ };
969
+
970
+ // src/navigation/geometry.ts
971
+ var EARTH_RADIUS_M = 63710088e-1;
972
+ var M_PER_DEG_LAT = Math.PI / 180 * EARTH_RADIUS_M;
973
+ var toRad = (deg) => deg * Math.PI / 180;
974
+ var toDeg = (rad) => rad * 180 / Math.PI;
975
+ function haversineM(a, b) {
976
+ const dLat = toRad(b[1] - a[1]);
977
+ const dLng = toRad(b[0] - a[0]);
978
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a[1])) * Math.cos(toRad(b[1])) * Math.sin(dLng / 2) ** 2;
979
+ return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
980
+ }
981
+ function bearingDeg(a, b) {
982
+ const phi1 = toRad(a[1]);
983
+ const phi2 = toRad(b[1]);
984
+ const dLambda = toRad(b[0] - a[0]);
985
+ const y = Math.sin(dLambda) * Math.cos(phi2);
986
+ const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLambda);
987
+ return (toDeg(Math.atan2(y, x)) + 360) % 360;
988
+ }
989
+ function angleDiffDeg(a, b) {
990
+ const d = Math.abs(((a - b) % 360 + 360) % 360);
991
+ return d > 180 ? 360 - d : d;
992
+ }
993
+ function projectOnSegment(p, a, b) {
994
+ const mPerDegLng = M_PER_DEG_LAT * Math.cos(toRad(a[1]));
995
+ const bx = (b[0] - a[0]) * mPerDegLng;
996
+ const by = (b[1] - a[1]) * M_PER_DEG_LAT;
997
+ const px = (p[0] - a[0]) * mPerDegLng;
998
+ const py = (p[1] - a[1]) * M_PER_DEG_LAT;
999
+ const len2 = bx * bx + by * by;
1000
+ const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, (px * bx + py * by) / len2));
1001
+ const qx = bx * t;
1002
+ const qy = by * t;
1003
+ const point = t === 1 ? [b[0], b[1]] : [a[0] + qx / mPerDegLng, a[1] + qy / M_PER_DEG_LAT];
1004
+ return { t, point, distance_m: Math.hypot(px - qx, py - qy) };
1005
+ }
1006
+ function cumulativeDistances(coords) {
1007
+ const cum = [];
1008
+ let total = 0;
1009
+ for (let i = 0; i < coords.length; i++) {
1010
+ const prev = coords[i - 1];
1011
+ const cur = coords[i];
1012
+ if (i > 0 && prev && cur) total += haversineM(prev, cur);
1013
+ cum.push(total);
1014
+ }
1015
+ return cum;
1016
+ }
1017
+
1018
+ // src/navigation/progress.ts
1019
+ function buildRouteIndex(route) {
1020
+ const coords = decodePolyline6(route.geometry);
1021
+ const cum = cumulativeDistances(coords);
1022
+ const total_m = cum[cum.length - 1] ?? 0;
1023
+ const at = (vertex) => cum[Math.min(Math.max(vertex, 0), cum.length - 1)] ?? 0;
1024
+ const steps = [];
1025
+ route.legs.forEach((leg, legIndex) => {
1026
+ leg.steps.forEach((step, indexInLeg) => {
1027
+ const begin_m = at(step.shape_begin);
1028
+ steps.push({ step, legIndex, indexInLeg, begin_m, end_m: begin_m });
1029
+ });
1030
+ });
1031
+ for (let i = 0; i < steps.length; i++) {
1032
+ const current = steps[i];
1033
+ if (current) current.end_m = steps[i + 1]?.begin_m ?? total_m;
1034
+ }
1035
+ return { coords, cum, total_m, steps, legBegin_m: route.legs.map((leg) => at(leg.shape_offset)) };
1036
+ }
1037
+ function stepAt(index, along_m) {
1038
+ const { steps, total_m } = index;
1039
+ if (steps.length === 0) return 0;
1040
+ if (along_m >= total_m) return steps.length - 1;
1041
+ let found = 0;
1042
+ for (let i = 0; i < steps.length; i++) {
1043
+ const s = steps[i];
1044
+ if (!s) continue;
1045
+ if (s.begin_m > along_m) break;
1046
+ if (along_m < s.end_m) {
1047
+ found = i;
1048
+ break;
1049
+ }
1050
+ found = i;
1051
+ }
1052
+ return found;
1053
+ }
1054
+ function progressAt(index, along_m) {
1055
+ const clamped = Math.max(0, Math.min(along_m, index.total_m));
1056
+ const stepIndex = stepAt(index, clamped);
1057
+ const current = index.steps[stepIndex];
1058
+ const next = index.steps[stepIndex + 1];
1059
+ let remaining_s = 0;
1060
+ if (current) {
1061
+ const length = current.end_m - current.begin_m;
1062
+ const fraction = length > 0 ? Math.max(0, Math.min(1, (current.end_m - clamped) / length)) : 0;
1063
+ remaining_s += fraction * current.step.duration_s;
1064
+ }
1065
+ for (let i = stepIndex + 1; i < index.steps.length; i++) {
1066
+ remaining_s += index.steps[i]?.step.duration_s ?? 0;
1067
+ }
1068
+ return {
1069
+ stepIndex,
1070
+ legIndex: current?.legIndex ?? 0,
1071
+ distanceToStep_m: next ? Math.max(0, next.begin_m - clamped) : 0,
1072
+ remaining_m: Math.max(0, index.total_m - clamped),
1073
+ remaining_s: Math.round(remaining_s)
1074
+ };
1075
+ }
1076
+
1077
+ // src/navigation/snap.ts
1078
+ var TIE_M = 10;
1079
+ var LOOKBACK_SEGMENTS = 2;
1080
+ function snapToRoute(index, p, opts) {
1081
+ const { coords, cum } = index;
1082
+ const segments = coords.length - 1;
1083
+ if (segments < 1) return null;
1084
+ let lo = 0;
1085
+ let hi = segments - 1;
1086
+ if (opts.fromShapeIndex !== null) {
1087
+ const from = Math.max(0, Math.min(opts.fromShapeIndex, segments - 1));
1088
+ lo = Math.max(0, from - LOOKBACK_SEGMENTS);
1089
+ const limit = (cum[from] ?? 0) + opts.window_m;
1090
+ hi = from;
1091
+ for (let i = from + 1; i < segments; i++) {
1092
+ if ((cum[i] ?? 0) <= limit) hi = i;
1093
+ else break;
1094
+ }
1095
+ }
1096
+ const heading = typeof opts.heading === "number" && Number.isFinite(opts.heading) ? opts.heading : null;
1097
+ let best = null;
1098
+ for (let i = lo; i <= hi; i++) {
1099
+ const a = coords[i];
1100
+ const b = coords[i + 1];
1101
+ if (!a || !b) continue;
1102
+ const proj = projectOnSegment(p, a, b);
1103
+ const segStart = cum[i] ?? 0;
1104
+ const segEnd = cum[i + 1] ?? segStart;
1105
+ const candidate = {
1106
+ shapeIndex: i,
1107
+ t: proj.t,
1108
+ point: proj.point,
1109
+ distance_m: proj.distance_m,
1110
+ along_m: segStart + proj.t * (segEnd - segStart)
1111
+ };
1112
+ if (!best) {
1113
+ best = candidate;
1114
+ continue;
1115
+ }
1116
+ const diff = candidate.distance_m - best.distance_m;
1117
+ const adjacent = candidate.shapeIndex - best.shapeIndex <= 1;
1118
+ if (!adjacent && Math.abs(diff) <= TIE_M) {
1119
+ best = preferByHeadingOrFurther(coords, best, candidate, heading);
1120
+ } else if (diff < 0) {
1121
+ best = candidate;
1122
+ }
1123
+ }
1124
+ return best;
1125
+ }
1126
+ function segmentBearing(coords, shapeIndex) {
1127
+ const a = coords[shapeIndex];
1128
+ const b = coords[shapeIndex + 1];
1129
+ return a && b ? bearingDeg(a, b) : 0;
1130
+ }
1131
+ function preferByHeadingOrFurther(coords, current, candidate, heading) {
1132
+ if (heading === null) return candidate;
1133
+ const dCurrent = angleDiffDeg(segmentBearing(coords, current.shapeIndex), heading);
1134
+ const dCandidate = angleDiffDeg(segmentBearing(coords, candidate.shapeIndex), heading);
1135
+ return dCandidate <= dCurrent ? candidate : current;
1136
+ }
1137
+
1138
+ // src/navigation/announce.ts
1139
+ var viNumber = (value) => value.replace(".", ",");
1140
+ function formatDistance(m, lang = "vi") {
1141
+ const vi = lang === "vi";
1142
+ if (m < 1e3) {
1143
+ const n2 = Math.max(0, Math.round(m));
1144
+ return vi ? `${n2} m\xE9t` : `${n2} meters`;
1145
+ }
1146
+ const km = m / 1e3;
1147
+ if (km < 10) {
1148
+ const s = km.toFixed(1);
1149
+ return vi ? `${viNumber(s)} ki-l\xF4-m\xE9t` : `${s} kilometers`;
1150
+ }
1151
+ const n = Math.round(km);
1152
+ return vi ? `${n} ki-l\xF4-m\xE9t` : `${n} kilometers`;
1153
+ }
1154
+ function formatDistanceShort(m) {
1155
+ if (m < 1e3) return `${Math.max(0, Math.round(m))} m`;
1156
+ const km = m / 1e3;
1157
+ return km < 10 ? `${viNumber(km.toFixed(1))} km` : `${Math.round(km)} km`;
1158
+ }
1159
+ function roundForSpeech(m) {
1160
+ if (m >= 200) return Math.round(m / 50) * 50;
1161
+ return Math.max(10, Math.round(m / 10) * 10);
1162
+ }
1163
+ function lowerFirst(text) {
1164
+ return text.length === 0 ? text : text.charAt(0).toLowerCase() + text.slice(1);
1165
+ }
1166
+ function composeApproach(distance_m, step, lang) {
1167
+ const cue = step.verbal_alert ?? step.verbal_pre;
1168
+ if (!cue) return null;
1169
+ const d = formatDistance(roundForSpeech(distance_m), lang);
1170
+ return lang === "vi" ? `Trong ${d} n\u1EEFa, ${lowerFirst(cue)}` : `In ${d}, ${lowerFirst(cue)}`;
1171
+ }
1172
+ function planAnnouncements(p, th, lang, announced, stepChanged) {
1173
+ const out = [];
1174
+ const push = (stepIndex, kind, text, priority) => {
1175
+ if (!text) return;
1176
+ const key = `${stepIndex}:${kind}`;
1177
+ if (announced.has(key)) return;
1178
+ announced.add(key);
1179
+ out.push({ text, kind, stepIndex, priority });
1180
+ };
1181
+ const longEnough = p.step.distance_m > th.approach_m + th.pre_m;
1182
+ if (p.step.kind === "depart") push(p.stepIndex, "depart", p.step.verbal_pre, 3);
1183
+ else if (stepChanged && longEnough) push(p.stepIndex, "post", p.step.verbal_post, 1);
1184
+ const next = p.nextStep;
1185
+ if (next) {
1186
+ const nextIndex = p.stepIndex + 1;
1187
+ const d = p.distanceToStep_m;
1188
+ if (d <= th.approach_m && longEnough) {
1189
+ push(nextIndex, "approach", composeApproach(d, next, lang), 2);
1190
+ }
1191
+ if (d <= th.pre_m)
1192
+ push(nextIndex, next.kind === "arrive" ? "arrive" : "pre", next.verbal_pre, 3);
1193
+ }
1194
+ return out;
1195
+ }
1196
+
1197
+ // src/navigation/simulate.ts
1198
+ var SIMULATE_DEFAULT_SPEED_MPS = {
1199
+ walk: 1.4,
1200
+ motorbike: 8,
1201
+ car: 12
1202
+ };
1203
+ var M_PER_DEG_LAT2 = Math.PI / 180 * 63710088e-1;
1204
+ function mulberry32(seed) {
1205
+ let a = seed >>> 0;
1206
+ return () => {
1207
+ a = a + 1831565813 >>> 0;
1208
+ let t = a;
1209
+ t = Math.imul(t ^ t >>> 15, t | 1);
1210
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1211
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1212
+ };
1213
+ }
1214
+ function simulateFixes(route, opts = {}) {
1215
+ const coords = decodePolyline6(route.geometry);
1216
+ const cum = cumulativeDistances(coords);
1217
+ const total = cum[cum.length - 1] ?? 0;
1218
+ const speed = opts.speed_mps ?? SIMULATE_DEFAULT_SPEED_MPS[route.mode];
1219
+ const interval = opts.interval_s ?? 1;
1220
+ const accuracy = opts.accuracy_m ?? 8;
1221
+ const jitter = opts.jitter_m ?? 0;
1222
+ const start = opts.start_ms ?? 17e11;
1223
+ const rand = mulberry32(opts.seed ?? 1);
1224
+ const stepM = speed * interval;
1225
+ if (coords.length === 0 || stepM <= 0) return [];
1226
+ const fixes = [];
1227
+ let segment = 0;
1228
+ const distances = [];
1229
+ for (let d = 0; d < total; d += stepM) distances.push(d);
1230
+ distances.push(total);
1231
+ distances.forEach((d, k) => {
1232
+ while (segment < coords.length - 2 && (cum[segment + 1] ?? 0) < d) segment += 1;
1233
+ const a = coords[segment];
1234
+ const b = coords[segment + 1] ?? a;
1235
+ if (!a || !b) return;
1236
+ const segStart = cum[segment] ?? 0;
1237
+ const segLen = (cum[segment + 1] ?? segStart) - segStart;
1238
+ const t = segLen > 0 ? Math.min(1, (d - segStart) / segLen) : 0;
1239
+ let lng = a[0] + (b[0] - a[0]) * t;
1240
+ let lat = a[1] + (b[1] - a[1]) * t;
1241
+ if (jitter > 0) {
1242
+ const angle = rand() * 2 * Math.PI;
1243
+ const radius = jitter * Math.sqrt(rand());
1244
+ lat += radius * Math.cos(angle) / M_PER_DEG_LAT2;
1245
+ lng += radius * Math.sin(angle) / (M_PER_DEG_LAT2 * Math.cos(lat * Math.PI / 180));
1246
+ }
1247
+ fixes.push({
1248
+ lng,
1249
+ lat,
1250
+ accuracy_m: accuracy,
1251
+ heading: bearingDeg(a, b),
1252
+ speed_mps: speed,
1253
+ timestamp: start + k * interval * 1e3
1254
+ });
1255
+ });
1256
+ return fixes;
1257
+ }
1258
+
1259
+ // src/navigation/navigator.ts
1260
+ var DEFAULT_ACCURACY_M = 10;
1261
+ var WINDOW_BASE_M = 300;
1262
+ var WINDOW_SPEED_MPS = 40;
1263
+ var WINDOW_MAX_M = 3e3;
1264
+ var ARRIVE_NEAR_REMAINING_M = 150;
1265
+ var MOVING_SPEED_MPS = 1;
1266
+ function buildState(response, routeIndex, override) {
1267
+ const route = response.routes[routeIndex];
1268
+ if (!route) throw new Error(`createNavigator: response kh\xF4ng c\xF3 routes[${routeIndex}]`);
1269
+ return {
1270
+ response,
1271
+ routeIndex,
1272
+ route,
1273
+ index: buildRouteIndex(route),
1274
+ th: { ...NAVIGATION_THRESHOLDS[route.mode], ...override }
1275
+ };
1276
+ }
1277
+ function createNavigator(opts) {
1278
+ const rerouteMode = opts.reroute ?? "auto";
1279
+ const provider = opts.provider;
1280
+ if (rerouteMode === "auto" && !provider) {
1281
+ throw new Error("createNavigator: reroute 'auto' c\u1EA7n provider (v\xED d\u1EE5 client c\u1EE7a createClient)");
1282
+ }
1283
+ const lang = opts.lang ?? "vi";
1284
+ let rs = buildState(opts.response, opts.routeIndex ?? 0, opts.thresholds);
1285
+ let status = "idle";
1286
+ let progress = null;
1287
+ let last = null;
1288
+ let maxAlong_m = 0;
1289
+ let offCount = 0;
1290
+ let offSince = null;
1291
+ let backCount = 0;
1292
+ let announced = /* @__PURE__ */ new Set();
1293
+ let rerouteAttempts = 0;
1294
+ let lastRerouteAt = null;
1295
+ let inflight = false;
1296
+ let rerouteToken = 0;
1297
+ let rerouteReason = null;
1298
+ const listeners = {
1299
+ status: /* @__PURE__ */ new Set(),
1300
+ progress: /* @__PURE__ */ new Set(),
1301
+ step: /* @__PURE__ */ new Set(),
1302
+ waypoint: /* @__PURE__ */ new Set(),
1303
+ offRoute: /* @__PURE__ */ new Set(),
1304
+ reroute: /* @__PURE__ */ new Set(),
1305
+ rerouteFailed: /* @__PURE__ */ new Set(),
1306
+ announce: /* @__PURE__ */ new Set(),
1307
+ arrive: /* @__PURE__ */ new Set()
1308
+ };
1309
+ const emit = (k, e) => {
1310
+ for (const fn of listeners[k]) fn(e);
1311
+ };
1312
+ const setStatus = (next) => {
1313
+ if (next === status) return;
1314
+ const previous = status;
1315
+ status = next;
1316
+ emit("status", { status, previous });
1317
+ };
1318
+ const resetTracking = () => {
1319
+ last = null;
1320
+ maxAlong_m = 0;
1321
+ offCount = 0;
1322
+ offSince = null;
1323
+ backCount = 0;
1324
+ announced = /* @__PURE__ */ new Set();
1325
+ };
1326
+ const applyRoute = (response, routeIndex) => {
1327
+ rs = buildState(response, routeIndex, opts.thresholds);
1328
+ resetTracking();
1329
+ progress = null;
1330
+ };
1331
+ const destinationLatLng = () => {
1332
+ const w = rs.response.waypoints[rs.response.waypoints.length - 1];
1333
+ const end = rs.index.coords[rs.index.coords.length - 1];
1334
+ const [lng, lat] = w ? w.location : end ?? [0, 0];
1335
+ return [lat, lng];
1336
+ };
1337
+ const rerouteRequest = (fix, legIndex) => {
1338
+ const via = rs.response.waypoints.slice(legIndex + 1, -1).map((w) => [w.location[1], w.location[0]]);
1339
+ const request = {
1340
+ from: [fix.lat, fix.lng],
1341
+ to: destinationLatLng(),
1342
+ mode: rs.route.mode,
1343
+ lang,
1344
+ alternatives: false
1345
+ };
1346
+ if (via.length > 0) request.via = via;
1347
+ return request;
1348
+ };
1349
+ async function runReroute(reason, fix, legIndex) {
1350
+ if (!provider) return;
1351
+ const token = ++rerouteToken;
1352
+ inflight = true;
1353
+ rerouteReason = reason;
1354
+ lastRerouteAt = fix.timestamp;
1355
+ setStatus("rerouting");
1356
+ try {
1357
+ const next = await provider.directions(rerouteRequest(fix, legIndex));
1358
+ inflight = false;
1359
+ if (token !== rerouteToken || status !== "rerouting") return;
1360
+ rerouteAttempts = 0;
1361
+ applyRoute(next, 0);
1362
+ emit("reroute", { reason, response: next });
1363
+ setStatus("navigating");
1364
+ } catch (error) {
1365
+ inflight = false;
1366
+ if (token !== rerouteToken) return;
1367
+ rerouteAttempts += 1;
1368
+ if (status === "rerouting") setStatus("off_route");
1369
+ emit("rerouteFailed", {
1370
+ error,
1371
+ attempts: rerouteAttempts,
1372
+ final: rerouteAttempts >= rs.th.rerouteMaxFailures
1373
+ });
1374
+ }
1375
+ }
1376
+ const maybeAutoReroute = (fix, legIndex) => {
1377
+ if (rerouteMode !== "auto" || !provider || inflight) return;
1378
+ if (rerouteAttempts >= rs.th.rerouteMaxFailures) return;
1379
+ if (lastRerouteAt !== null && fix.timestamp - lastRerouteAt < rs.th.rerouteCooldown_s * 1e3) {
1380
+ return;
1381
+ }
1382
+ void runReroute("off_route", fix, legIndex);
1383
+ };
1384
+ const segmentBearing2 = (shapeIndex) => {
1385
+ const a = rs.index.coords[shapeIndex];
1386
+ const b = rs.index.coords[shapeIndex + 1];
1387
+ return a && b ? bearingDeg(a, b) : 0;
1388
+ };
1389
+ function update(fix) {
1390
+ if (status === "arrived" || status === "stopped") return;
1391
+ const accuracy = fix.accuracy_m ?? DEFAULT_ACCURACY_M;
1392
+ if (accuracy > rs.th.maxAccuracy_m) return;
1393
+ const prev = last;
1394
+ if (prev && fix.timestamp <= prev.fix.timestamp) return;
1395
+ if (status === "idle") setStatus("navigating");
1396
+ const dt_s = prev ? (fix.timestamp - prev.fix.timestamp) / 1e3 : 0;
1397
+ const window_m = Math.min(WINDOW_MAX_M, WINDOW_BASE_M + WINDOW_SPEED_MPS * dt_s);
1398
+ const here = [fix.lng, fix.lat];
1399
+ const anchorShapeIndex = prev ? prev.shapeIndex : null;
1400
+ const snap = snapToRoute(rs.index, here, {
1401
+ fromShapeIndex: anchorShapeIndex,
1402
+ window_m,
1403
+ heading: fix.heading
1404
+ });
1405
+ if (!snap) return;
1406
+ let along_m = snap.along_m;
1407
+ let skippedViaStep = null;
1408
+ const currentLeg = prev?.legIndex ?? 0;
1409
+ const nextLegBegin = rs.index.legBegin_m[currentLeg + 1];
1410
+ const nextVia = rs.response.waypoints[currentLeg + 1];
1411
+ if (nextLegBegin !== void 0 && nextVia && currentLeg + 1 < rs.route.legs.length && along_m < nextLegBegin && haversineM(here, nextVia.snapped) <= rs.th.arrive_m) {
1412
+ along_m = nextLegBegin;
1413
+ skippedViaStep = rs.index.steps.find((s) => s.legIndex === currentLeg && s.step.kind === "arrive") ?? null;
1414
+ }
1415
+ const threshold = Math.max(rs.th.offRoute_m, 1.5 * accuracy);
1416
+ const perpendicularOk = snap.distance_m <= threshold;
1417
+ if (perpendicularOk && prev && along_m < maxAlong_m - rs.th.offRoute_m) backCount += 1;
1418
+ else backCount = 0;
1419
+ const onRoute = perpendicularOk && backCount < rs.th.offRouteFixes;
1420
+ if (onRoute) {
1421
+ offCount = 0;
1422
+ offSince = null;
1423
+ maxAlong_m = prev ? Math.max(maxAlong_m, along_m) : along_m;
1424
+ if (status === "off_route" || status === "rerouting" && rerouteReason === "off_route") {
1425
+ rerouteAttempts = 0;
1426
+ setStatus("navigating");
1427
+ }
1428
+ } else {
1429
+ offCount += 1;
1430
+ offSince ??= fix.timestamp;
1431
+ if (status === "navigating" && offCount >= rs.th.offRouteFixes && fix.timestamp - offSince >= rs.th.offRouteSeconds * 1e3) {
1432
+ setStatus("off_route");
1433
+ emit("offRoute", { distance_m: snap.distance_m, fix });
1434
+ }
1435
+ }
1436
+ const displayAlong_m = Math.max(along_m, maxAlong_m);
1437
+ const at = progressAt(rs.index, displayAlong_m);
1438
+ const rawFlat = rs.index.steps[at.stepIndex];
1439
+ if (!rawFlat) return;
1440
+ const rawNextFlat = rs.index.steps[at.stepIndex + 1];
1441
+ const moving = (fix.speed_mps ?? 0) > MOVING_SPEED_MPS;
1442
+ const bearing = moving && typeof fix.heading === "number" && Number.isFinite(fix.heading) ? fix.heading : segmentBearing2(snap.shapeIndex);
1443
+ const end = rs.index.coords[rs.index.coords.length - 1];
1444
+ const lastLeg = at.legIndex === rs.route.legs.length - 1;
1445
+ const nearEnd = end !== void 0 && lastLeg && haversineM(here, end) <= rs.th.arrive_m && at.remaining_m <= ARRIVE_NEAR_REMAINING_M;
1446
+ const arrivingNow = status === "navigating" && (at.remaining_m <= rs.th.arrive_m || nearEnd);
1447
+ const rawStepChanged = prev === null || prev.stepIndex !== at.stepIndex;
1448
+ const announceProgress = {
1449
+ status,
1450
+ route: rs.route,
1451
+ routeIndex: rs.routeIndex,
1452
+ legIndex: at.legIndex,
1453
+ stepIndex: at.stepIndex,
1454
+ step: rawFlat.step,
1455
+ nextStep: rawNextFlat ? rawNextFlat.step : null,
1456
+ snapped: snap.point,
1457
+ bearing,
1458
+ shapeIndex: snap.shapeIndex,
1459
+ traveled_m: displayAlong_m,
1460
+ remaining_m: at.remaining_m,
1461
+ remaining_s: at.remaining_s,
1462
+ distanceToStep_m: at.distanceToStep_m,
1463
+ offRoute_m: snap.distance_m,
1464
+ fix
1465
+ };
1466
+ const finalIndex = rs.index.steps.length - 1;
1467
+ const finalFlat = rs.index.steps[finalIndex];
1468
+ const effectiveAt = arrivingNow && finalFlat ? {
1469
+ stepIndex: finalIndex,
1470
+ legIndex: finalFlat.legIndex,
1471
+ distanceToStep_m: 0,
1472
+ remaining_m: 0,
1473
+ remaining_s: 0
1474
+ } : at;
1475
+ const flat = arrivingNow && finalFlat ? finalFlat : rawFlat;
1476
+ const nextFlat = arrivingNow ? void 0 : rawNextFlat;
1477
+ const stepChanged = prev === null || prev.stepIndex !== effectiveAt.stepIndex;
1478
+ const legChanged = prev !== null && effectiveAt.legIndex > prev.legIndex;
1479
+ progress = {
1480
+ status,
1481
+ route: rs.route,
1482
+ routeIndex: rs.routeIndex,
1483
+ legIndex: effectiveAt.legIndex,
1484
+ stepIndex: effectiveAt.stepIndex,
1485
+ step: flat.step,
1486
+ nextStep: nextFlat ? nextFlat.step : null,
1487
+ snapped: snap.point,
1488
+ bearing,
1489
+ shapeIndex: snap.shapeIndex,
1490
+ traveled_m: arrivingNow ? rs.index.total_m : displayAlong_m,
1491
+ remaining_m: effectiveAt.remaining_m,
1492
+ remaining_s: effectiveAt.remaining_s,
1493
+ distanceToStep_m: effectiveAt.distanceToStep_m,
1494
+ offRoute_m: snap.distance_m,
1495
+ fix
1496
+ };
1497
+ last = {
1498
+ fix,
1499
+ // Neo cửa sổ chỉ đi theo fix ĐANG ở trên tuyến; fix lệch giữ nguyên neo tốt gần nhất.
1500
+ shapeIndex: onRoute ? snap.shapeIndex : anchorShapeIndex ?? snap.shapeIndex,
1501
+ along_m: arrivingNow ? rs.index.total_m : displayAlong_m,
1502
+ stepIndex: effectiveAt.stepIndex,
1503
+ legIndex: effectiveAt.legIndex
1504
+ };
1505
+ if (status === "navigating") {
1506
+ if (stepChanged && prev !== null) {
1507
+ emit("step", { stepIndex: effectiveAt.stepIndex, step: flat.step });
1508
+ }
1509
+ if (legChanged) {
1510
+ const waypoint = rs.response.waypoints[effectiveAt.legIndex];
1511
+ if (waypoint) emit("waypoint", { legIndex: effectiveAt.legIndex, waypoint });
1512
+ }
1513
+ }
1514
+ emit("progress", progress);
1515
+ if (status === "navigating") {
1516
+ if (skippedViaStep) {
1517
+ const key = `via:${currentLeg}:arrive`;
1518
+ const text = skippedViaStep.step.verbal_pre;
1519
+ if (text && !announced.has(key)) {
1520
+ announced.add(key);
1521
+ const viaStepIndex = rs.index.steps.indexOf(skippedViaStep);
1522
+ emit("announce", { text, kind: "arrive", stepIndex: viaStepIndex, priority: 3 });
1523
+ }
1524
+ }
1525
+ for (const a of planAnnouncements(announceProgress, rs.th, lang, announced, rawStepChanged)) {
1526
+ emit("announce", a);
1527
+ }
1528
+ if (arrivingNow) {
1529
+ setStatus("arrived");
1530
+ progress = { ...progress, status };
1531
+ const waypoint = rs.response.waypoints[rs.response.waypoints.length - 1];
1532
+ if (waypoint) emit("arrive", { waypoint, fix });
1533
+ }
1534
+ } else if (status === "off_route") {
1535
+ maybeAutoReroute(fix, effectiveAt.legIndex);
1536
+ }
1537
+ }
1538
+ return {
1539
+ get status() {
1540
+ return status;
1541
+ },
1542
+ get progress() {
1543
+ return progress;
1544
+ },
1545
+ update,
1546
+ setRoute(response, routeIndex = 0) {
1547
+ rerouteToken += 1;
1548
+ inflight = false;
1549
+ rerouteAttempts = 0;
1550
+ lastRerouteAt = null;
1551
+ applyRoute(response, routeIndex);
1552
+ if (status === "off_route" || status === "rerouting") setStatus("navigating");
1553
+ },
1554
+ async reroute() {
1555
+ if (!provider) throw new Error("createNavigator: kh\xF4ng c\xF3 provider \u0111\u1EC3 t\xEDnh l\u1EA1i");
1556
+ if (status === "arrived" || status === "stopped") return;
1557
+ if (!last) throw new Error("createNavigator: ch\u01B0a c\xF3 v\u1ECB tr\xED \u0111\u1EC3 t\xEDnh l\u1EA1i");
1558
+ await runReroute("manual", last.fix, last.legIndex);
1559
+ },
1560
+ stop() {
1561
+ rerouteToken += 1;
1562
+ inflight = false;
1563
+ setStatus("stopped");
1564
+ },
1565
+ on(event, handler) {
1566
+ listeners[event].add(handler);
1567
+ },
1568
+ off(event, handler) {
1569
+ listeners[event].delete(handler);
1570
+ }
1571
+ };
1572
+ }
1573
+
1574
+ // src/navigation/route-features.ts
1575
+ var EMPTY_ROUTE_FEATURES = {
1576
+ type: "FeatureCollection",
1577
+ features: []
1578
+ };
1579
+ function decodeRoutes(response) {
1580
+ return response.routes.map((route) => decodePolyline6(route.geometry));
1581
+ }
1582
+ var line = (kind, index, coordinates) => ({
1583
+ type: "Feature",
1584
+ geometry: { type: "LineString", coordinates },
1585
+ properties: { kind, index }
1586
+ });
1587
+ function routeFeatures(coords, opts) {
1588
+ const progress = opts.progress ?? null;
1589
+ const features = [];
1590
+ for (const [i, c] of coords.entries()) {
1591
+ if (i !== opts.active) {
1592
+ features.push(line("alt", i, [...c]));
1593
+ continue;
1594
+ }
1595
+ if (progress && progress.shapeIndex < c.length - 1) {
1596
+ features.push(
1597
+ line("traveled", i, [...c.slice(0, progress.shapeIndex + 1), progress.snapped]),
1598
+ line("active", i, [progress.snapped, ...c.slice(progress.shapeIndex + 1)])
1599
+ );
1600
+ } else {
1601
+ features.push(line("active", i, [...c]));
1602
+ }
1603
+ }
1604
+ if (opts.puck && progress) {
1605
+ features.push({
1606
+ type: "Feature",
1607
+ geometry: { type: "Point", coordinates: progress.snapped },
1608
+ properties: { kind: "puck", bearing: progress.bearing ?? 0 }
1609
+ });
1610
+ }
1611
+ return { type: "FeatureCollection", features };
1612
+ }
795
1613
  export {
796
1614
  ATTRIBUTION_LINKS,
797
1615
  DEFAULT_POI_SOURCES,
1616
+ EMPTY_ROUTE_FEATURES,
1617
+ FIRST_SYMBOL_LAYER_ID,
1618
+ MANEUVER_KINDS,
798
1619
  MapsLibVNError,
799
1620
  NAME_FILLERS,
1621
+ NAVIGATION_THRESHOLDS,
800
1622
  POI_LAYER_ID,
801
1623
  POI_SOURCES,
802
1624
  POI_SOURCE_PROFILES,
1625
+ SIMULATE_DEFAULT_SPEED_MPS,
803
1626
  TOPONYM_ALIAS,
1627
+ VALHALLA_MANEUVER_KIND,
804
1628
  adminAliasKeys,
1629
+ angleDiffDeg,
805
1630
  applyBrandAlias,
806
1631
  applyToponymAlias,
807
1632
  attributionHtml,
808
1633
  attributionText,
1634
+ bearingDeg,
1635
+ buildRouteIndex,
1636
+ composeApproach,
809
1637
  createClient,
1638
+ createNavigator,
1639
+ cumulativeDistances,
1640
+ decodePolyline6,
1641
+ decodeRoutes,
1642
+ encodePolyline6,
810
1643
  expandAbbrev,
811
1644
  filterNameAlt,
812
1645
  foldTelex,
1646
+ formatDistance,
1647
+ formatDistanceShort,
1648
+ haversineM,
813
1649
  hidePoiLayer,
814
1650
  isNameLabelLayer,
815
1651
  isPoiStyleLayer,
816
1652
  localizeStyle,
817
1653
  looksLikeTelex,
1654
+ lowerFirst,
1655
+ maneuverKindFromValhalla,
1656
+ mulberry32,
818
1657
  nameCore,
819
1658
  nameExpression,
820
1659
  normalizePoiSources,
821
1660
  normalizeVi,
822
1661
  parseAddress,
823
1662
  parsePoiSourcesCsv,
1663
+ planAnnouncements,
824
1664
  poiSourceClause,
825
1665
  poiSourcesKey,
826
1666
  profileForSources,
1667
+ progressAt,
1668
+ projectOnSegment,
1669
+ roundForSpeech,
1670
+ routeFeatures,
827
1671
  searchKeys,
1672
+ simulateFixes,
1673
+ snapToRoute,
1674
+ stepAt,
828
1675
  stripDiacritics,
829
1676
  viKey
830
1677
  };