PageSourceSearch

https://stridenavigation.com/chunk-CZMIP4ZQ.js

js stridenavigation.com collected 2026-09-24 14:54:12 UTC 172,673 bytes, 3,885 lines download raw bytes

1import {
2  APP_VERSION
3} from "./chunk-ODH7QDZO.js";
4import {
5  DirectionType,
6  NavigationMode,
7  NavigationRoute,
8  NavigationStateManagerService,
9  RouteOverlayType
10} from "./chunk-4UEQF233.js";
11import {
12  CampusConfigurationService,
13  LocationService,
14  cloneDefaultRoutePenalties
15} from "./chunk-K52XM66R.js";
16import {
17  createGPSTransform,
18  transformGPSToMap
19} from "./chunk-PYYASMRY.js";
20import {
21  DebugControlService
22} from "./chunk-PFRFKH6U.js";
23import {
24  LocationSource
25} from "./chunk-2E4THSOE.js";
26import {
27  SettingsService
28} from "./chunk-QI7JV4VS.js";
29import {
30  DatabaseService,
31  EntityType,
32  NodeModel,
33  StairwellModel
34} from "./chunk-RLJCY5HS.js";
35import {
36  TranslateService
37} from "./chunk-O5XYYQCR.js";
38import {
39  Node
40} from "./chunk-QGEJYSW7.js";
41import {
42  environment
43} from "./chunk-L4O6SE3A.js";
44import {
45  AlertController
46} from "./chunk-INK5THDW.js";
47import {
48  BehaviorSubject,
49  Injectable,
50  Subject,
51  distinctUntilChanged,
52  filter,
53  map,
54  setClassMetadata,
55  shareReplay,
56  takeUntil,
57  timer,
58  ɵɵdefineInjectable,
59  ɵɵinject
60} from "./chunk-UQ6LLOJ6.js";
61import {
62  Capacitor
63} from "./chunk-5LKHIIQE.js";
64import {
65  __async,
66  __objRest,
67  __spreadProps,
68  __spreadValues
69} from "./chunk-CO64IJQE.js";
70
71// src/app/common/math-helpers.ts
72var _MathHelpers = class _MathHelpers {
73  static distance(point1, point2) {
74    const deltaX = point2.x - point1.x;
75    const deltaY = point2.y - point1.y;
76    return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
77  }
78  /**
79   * Calculate Euclidean distance between two points
80   */
81  static calculateDistance(p1, p2) {
82    const dx = p2.x - p1.x;
83    const dy = p2.y - p1.y;
84    return Math.sqrt(dx * dx + dy * dy);
85  }
86  /**
87   * Calculate perpendicular distance from a point to a line segment
88   */
89  static pointToLineDistance(point, lineStart, lineEnd) {
90    const A = point.x - lineStart.x;
91    const B = point.y - lineStart.y;
92    const C = lineEnd.x - lineStart.x;
93    const D = lineEnd.y - lineStart.y;
94    const dot = A * C + B * D;
95    const lenSq = C * C + D * D;
96    let param = -1;
97    if (lenSq !== 0) {
98      param = dot / lenSq;
99    }
100    let xx, yy;
101    if (param < 0) {
102      xx = lineStart.x;
103      yy = lineStart.y;
104    } else if (param > 1) {
105      xx = lineEnd.x;
106      yy = lineEnd.y;
107    } else {
108      xx = lineStart.x + param * C;
109      yy = lineStart.y + param * D;
110    }
111    const dx = point.x - xx;
112    const dy = point.y - yy;
113    return Math.sqrt(dx * dx + dy * dy);
114  }
115};
116_MathHelpers.interpolatePointBySignal = (pointA, pointB) => {
117  const totalSignal = pointA.signal + pointB.signal;
118  if (totalSignal === 0) {
119    return {
120      x: (pointA.x + pointB.x) / 2,
121      y: (pointA.y + pointB.y) / 2
122    };
123  }
124  const x = (pointA.x * pointA.signal + pointB.x * pointB.signal) / totalSignal;
125  const y = (pointA.y * pointA.signal + pointB.y * pointB.signal) / totalSignal;
126  return { x, y };
127};
128_MathHelpers.interpolatePointByDistance = (pointA, pointB) => {
129  const totalSignal = pointA.distance + pointB.distance;
130  if (totalSignal === 0) {
131    return {
132      x: (pointA.x + pointB.x) / 2,
133      y: (pointA.y + pointB.y) / 2
134    };
135  }
136  const x = (pointA.x * pointB.distance + pointB.x * pointA.distance) / totalSignal;
137  const y = (pointA.y * pointB.distance + pointB.y * pointA.distance) / totalSignal;
138  return { x, y };
139};
140var MathHelpers = _MathHelpers;
141
142// src/app/services/navigation-diagnostics/navigation-diagnostics.service.ts
143var _NavigationDiagnosticsService = class _NavigationDiagnosticsService {
144  constructor() {
145    this.events = [];
146    this.voiceSuppressions = /* @__PURE__ */ new Set();
147    this.nextId = 1;
148  }
149  record(category, event, details, correlationId) {
150    this.events.push(__spreadValues(__spreadValues({
151      timestamp: (/* @__PURE__ */ new Date()).toISOString(),
152      category,
153      event
154    }, correlationId ? { correlationId } : {}), details && Object.keys(details).length > 0 ? { details: this.cloneDetails(details) } : {}));
155    if (this.events.length > _NavigationDiagnosticsService.CAPACITY) {
156      this.events.splice(0, this.events.length - _NavigationDiagnosticsService.CAPACITY);
157    }
158  }
159  recordVoiceSuppression(cueId, reason, details) {
160    const key = `${cueId}|${reason}`;
161    if (this.voiceSuppressions.has(key)) {
162      return;
163    }
164    this.voiceSuppressions.add(key);
165    this.record("voice", "cue-suppressed", __spreadValues({ cueId, reason }, details), cueId);
166  }
167  resetVoiceSuppressionDedupe() {
168    this.voiceSuppressions.clear();
169  }
170  nextCorrelationId(prefix) {
171    return `${prefix}-${Date.now().toString(36)}-${this.nextId++}`;
172  }
173  snapshot() {
174    return this.events.map((event) => __spreadValues(__spreadValues({}, event), event.details ? { details: this.cloneDetails(event.details) } : {}));
175  }
176  clear() {
177    this.events.length = 0;
178    this.voiceSuppressions.clear();
179  }
180  cloneDetails(details) {
181    return Object.fromEntries(Object.entries(details).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]));
182  }
183};
184_NavigationDiagnosticsService.CAPACITY = 200;
185_NavigationDiagnosticsService.\u0275fac = function NavigationDiagnosticsService_Factory(__ngFactoryType__) {
186  return new (__ngFactoryType__ || _NavigationDiagnosticsService)();
187};
188_NavigationDiagnosticsService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _NavigationDiagnosticsService, factory: _NavigationDiagnosticsService.\u0275fac, providedIn: "root" });
189var NavigationDiagnosticsService = _NavigationDiagnosticsService;
190(() => {
191  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NavigationDiagnosticsService, [{
192    type: Injectable,
193    args: [{ providedIn: "root" }]
194  }], null, null);
195})();
196
197// src/app/common/map-renderer-v2/utils/campus-geo-reference.ts
198function createCampusGeoReference(campus, floors) {
199  const referencePoints = collectCampusReferencePoints(campus, floors);
200  if (referencePoints.length < 2) {
201    return null;
202  }
203  try {
204    const transform = createGPSTransform(referencePoints);
205    return {
206      transform,
207      quality: transform.quality,
208      referencePoints
209    };
210  } catch (error) {
211    console.warn("[MapRendererV2] Failed to build OSM geo-reference", error);
212    return null;
213  }
214}
215function collectCampusReferencePoints(campus, floors) {
216  var _a, _b, _c, _d, _e, _f, _g;
217  const referencePoints = [];
218  const seenNodeIds = /* @__PURE__ */ new Set();
219  const addNode = (node) => {
220    const id = node == null ? void 0 : node.entity.id;
221    if (!node || !id || seenNodeIds.has(id) || node.entity.latitude == null || node.entity.longitude == null || node.entity.locationX == null || node.entity.locationY == null) {
222      return;
223    }
224    referencePoints.push({
225      gps: {
226        latitude: node.entity.latitude,
227        longitude: node.entity.longitude
228      },
229      map: {
230        x: node.entity.locationX,
231        y: node.entity.locationY
232      },
233      id
234    });
235    seenNodeIds.add(id);
236  };
237  for (const node of (_a = campus == null ? void 0 : campus.nodes) != null ? _a : []) {
238    addNode(node);
239  }
240  for (const floor of (_b = campus == null ? void 0 : campus.floors) != null ? _b : []) {
241    for (const node of (_c = floor.nodes) != null ? _c : []) {
242      addNode(node);
243    }
244  }
245  for (const building of (_d = campus == null ? void 0 : campus.buildings) != null ? _d : []) {
246    for (const floor of (_e = building.floors) != null ? _e : []) {
247      for (const node of (_f = floor.nodes) != null ? _f : []) {
248        addNode(node);
249      }
250    }
251  }
252  for (const floor of floors != null ? floors : []) {
253    for (const node of (_g = floor.nodes) != null ? _g : []) {
254      addNode(node);
255    }
256  }
257  return referencePoints;
258}
259
260// src/app/services/navigation-instructions/navigation-instruction.models.ts
261var DEFAULT_NAVIGATION_INSTRUCTION_CONFIG = {
262  straightAngleToleranceDeg: 20,
263  slightTurnAngleDeg: 45,
264  turnAngleDeg: 135,
265  minTurnSegmentCm: 150,
266  minInitialTurnDistanceCm: 300,
267  upcomingInstructionLimit: 3,
268  indoorFarThresholdCm: 2e3,
269  indoorApproachThresholdCm: 200,
270  outdoorFarThresholdCm: 5e3,
271  outdoorApproachThresholdCm: 300
272};
273
274// src/app/services/navigation-instructions/navigation-instruction-classifier.ts
275function isActionInstruction(type) {
276  return type !== DirectionType.Continue && type !== DirectionType.Start;
277}
278function isTurnInstruction(type) {
279  return type === DirectionType.TurnLeft || type === DirectionType.TurnRight || type === DirectionType.SlightLeft || type === DirectionType.SlightRight;
280}
281
282// src/app/services/navigation-instructions/route-instruction-planner.service.ts
283var _RouteInstructionPlannerService = class _RouteInstructionPlannerService {
284  constructor() {
285    this.config = DEFAULT_NAVIGATION_INSTRUCTION_CONFIG;
286    this.routeStepsCache = /* @__PURE__ */ new WeakMap();
287  }
288  getSteps(route) {
289    if (!route || route.segments.length === 0) {
290      return [];
291    }
292    const cachedSteps = this.routeStepsCache.get(route);
293    if (cachedSteps) {
294      return cachedSteps;
295    }
296    const cumulativeDistances = this.getCumulativeSegmentDistances(route.segments);
297    const steps = route.directions.map((direction, directionIndex) => this.toStep(route, direction, directionIndex, cumulativeDistances)).filter((step) => step !== null).filter((step) => this.isHighQualityStep(route, step)).sort((a, b) => a.routeDistanceCm - b.routeDistanceCm);
298    const consolidatedSteps = this.consolidateAdjacentTurns(steps);
299    const normalizedSteps = this.normalizeStepDistances(consolidatedSteps);
300    this.routeStepsCache.set(route, normalizedSteps);
301    return normalizedSteps;
302  }
303  toStep(route, direction, directionIndex, cumulativeDistances) {
304    var _a, _b;
305    const segmentIndex = this.findSegmentIndexForDirection(route, direction, directionIndex);
306    const routeDistanceCm = this.getDirectionRouteDistance(route, direction, segmentIndex, cumulativeDistances);
307    const nodeId = (_b = (_a = direction.node) == null ? void 0 : _a.entity) == null ? void 0 : _b.id;
308    if (!nodeId || !Number.isFinite(routeDistanceCm)) {
309      return null;
310    }
311    return {
312      id: this.createStepId(direction, nodeId, segmentIndex, routeDistanceCm),
313      type: direction.type,
314      node: direction.node,
315      nodeId,
316      segmentIndex,
317      routeDistanceCm,
318      distanceCm: direction.distance,
319      destinationFloorLevel: direction.destinationFloorLevel
320    };
321  }
322  findSegmentIndexForDirection(route, direction, directionIndex) {
323    var _a, _b;
324    const mappedIndex = route.segmentToDirectionMap.findIndex((index) => index === directionIndex);
325    if (mappedIndex >= 0) {
326      return mappedIndex;
327    }
328    const nodeId = (_b = (_a = direction.node) == null ? void 0 : _a.entity) == null ? void 0 : _b.id;
329    if (nodeId) {
330      const startIndex = route.segments.findIndex((segment) => segment.startNode.entity.id === nodeId);
331      if (startIndex >= 0) {
332        return startIndex;
333      }
334      const endIndex = route.segments.findIndex((segment) => segment.endNode.entity.id === nodeId);
335      if (endIndex >= 0) {
336        return endIndex;
337      }
338    }
339    return Math.max(0, Math.min(directionIndex, route.segments.length - 1));
340  }
341  getDirectionRouteDistance(route, direction, segmentIndex, cumulativeDistances) {
342    var _a;
343    if (direction.type === DirectionType.Arrive) {
344      return route.totalDistance;
345    }
346    const mappedNodeDistance = this.getMappedSegmentNodeDistance(route.segments, direction.node, segmentIndex, cumulativeDistances);
347    if (mappedNodeDistance !== null) {
348      return mappedNodeDistance;
349    }
350    const nodeDistance = this.getNodeRouteDistance(route.segments, direction.node, cumulativeDistances);
351    if (nodeDistance !== null) {
352      return nodeDistance;
353    }
354    return (_a = cumulativeDistances[Math.max(0, Math.min(segmentIndex, cumulativeDistances.length - 1))]) != null ? _a : 0;
355  }
356  getMappedSegmentNodeDistance(segments, node, segmentIndex, cumulativeDistances) {
357    var _a;
358    const segment = segments[segmentIndex];
359    const nodeId = (_a = node == null ? void 0 : node.entity) == null ? void 0 : _a.id;
360    const segmentStartDistance = cumulativeDistances[segmentIndex];
361    if (!segment || !nodeId || !Number.isFinite(segmentStartDistance)) {
362      return null;
363    }
364    if (segment.startNode.entity.id === nodeId) {
365      return segmentStartDistance;
366    }
367    if (segment.endNode.entity.id === nodeId) {
368      return segmentStartDistance + segment.distance;
369    }
370    return null;
371  }
372  getNodeRouteDistance(segments, node, cumulativeDistances) {
373    var _a;
374    const nodeId = (_a = node == null ? void 0 : node.entity) == null ? void 0 : _a.id;
375    if (!nodeId) {
376      return null;
377    }
378    for (let i = 0; i < segments.length; i++) {
379      if (segments[i].startNode.entity.id === nodeId) {
380        return cumulativeDistances[i];
381      }
382      if (segments[i].endNode.entity.id === nodeId) {
383        return cumulativeDistances[i] + segments[i].distance;
384      }
385    }
386    return null;
387  }
388  getCumulativeSegmentDistances(segments) {
389    const cumulativeDistances = [];
390    let totalDistance = 0;
391    for (const segment of segments) {
392      cumulativeDistances.push(totalDistance);
393      totalDistance += segment.distance;
394    }
395    return cumulativeDistances;
396  }
397  isHighQualityStep(route, step) {
398    if (!isTurnInstruction(step.type)) {
399      return true;
400    }
401    if (step.routeDistanceCm < this.config.minInitialTurnDistanceCm) {
402      return false;
403    }
404    const prevSegment = route.segments[step.segmentIndex - 1];
405    const currentSegment = route.segments[step.segmentIndex];
406    if (!prevSegment || !currentSegment) {
407      return true;
408    }
409    if (prevSegment.distance < this.config.minTurnSegmentCm || currentSegment.distance < this.config.minTurnSegmentCm) {
410      const stableAngle = this.calculateStableTurnAngle(route.segments, step.segmentIndex);
411      return Math.abs(stableAngle) >= this.config.straightAngleToleranceDeg;
412    }
413    const angle = this.calculateTurnAngle(prevSegment.startNode, prevSegment.endNode, currentSegment.endNode);
414    return Math.abs(angle) >= this.config.straightAngleToleranceDeg;
415  }
416  consolidateAdjacentTurns(steps) {
417    const consolidated = [];
418    for (const step of steps) {
419      const previous = consolidated[consolidated.length - 1];
420      const isSameTurnCluster = previous && isTurnInstruction(previous.type) && previous.type === step.type && step.routeDistanceCm - previous.routeDistanceCm < this.config.minTurnSegmentCm;
421      if (!isSameTurnCluster) {
422        consolidated.push(step);
423        continue;
424      }
425    }
426    return consolidated;
427  }
428  normalizeStepDistances(steps) {
429    return steps.map((step, index) => {
430      var _a, _b;
431      return __spreadProps(__spreadValues({}, step), {
432        distanceCm: Math.max(0, ((_b = (_a = steps[index + 1]) == null ? void 0 : _a.routeDistanceCm) != null ? _b : step.routeDistanceCm) - step.routeDistanceCm)
433      });
434    });
435  }
436  calculateStableTurnAngle(segments, segmentIndex) {
437    const incoming = this.findStableIncomingSegment(segments, segmentIndex);
438    const outgoing = this.findStableOutgoingSegment(segments, segmentIndex);
439    if (!incoming || !outgoing) {
440      return 0;
441    }
442    return this.calculateTurnAngle(incoming.startNode, incoming.endNode, outgoing.endNode);
443  }
444  findStableIncomingSegment(segments, segmentIndex) {
445    var _a;
446    for (let i = segmentIndex - 1; i >= 0; i--) {
447      if (segments[i].distance >= this.config.minTurnSegmentCm) {
448        return segments[i];
449      }
450    }
451    return (_a = segments[segmentIndex - 1]) != null ? _a : null;
452  }
453  findStableOutgoingSegment(segments, segmentIndex) {
454    var _a;
455    for (let i = segmentIndex; i < segments.length; i++) {
456      if (segments[i].distance >= this.config.minTurnSegmentCm) {
457        return segments[i];
458      }
459    }
460    return (_a = segments[segmentIndex]) != null ? _a : null;
461  }
462  calculateTurnAngle(from, via, to) {
463    const angle1 = Math.atan2((via.entity.locationY || 0) - (from.entity.locationY || 0), (via.entity.locationX || 0) - (from.entity.locationX || 0));
464    const angle2 = Math.atan2((to.entity.locationY || 0) - (via.entity.locationY || 0), (to.entity.locationX || 0) - (via.entity.locationX || 0));
465    let angle = (angle2 - angle1) * (180 / Math.PI);
466    while (angle > 180)
467      angle -= 360;
468    while (angle < -180)
469      angle += 360;
470    return angle;
471  }
472  createStepId(direction, nodeId, segmentIndex, routeDistanceCm) {
473    var _a;
474    const floor = (_a = direction.destinationFloorLevel) != null ? _a : "no-floor";
475    const distanceBucket = Math.round(routeDistanceCm / 100);
476    return `${nodeId}|${direction.type}|${segmentIndex}|${floor}|${distanceBucket}`;
477  }
478};
479_RouteInstructionPlannerService.\u0275fac = function RouteInstructionPlannerService_Factory(__ngFactoryType__) {
480  return new (__ngFactoryType__ || _RouteInstructionPlannerService)();
481};
482_RouteInstructionPlannerService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _RouteInstructionPlannerService, factory: _RouteInstructionPlannerService.\u0275fac, providedIn: "root" });
483var RouteInstructionPlannerService = _RouteInstructionPlannerService;
484(() => {
485  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(RouteInstructionPlannerService, [{
486    type: Injectable,
487    args: [{
488      providedIn: "root"
489    }]
490  }], null, null);
491})();
492
493// src/app/services/navigation-instructions/navigation-instruction-presenter.service.ts
494var _NavigationInstructionPresenterService = class _NavigationInstructionPresenterService {
495  constructor(translateService) {
496    this.translateService = translateService;
497  }
498  present(step, phase, distanceToActionCm) {
499    const distanceMeters = Math.max(0, Math.round(distanceToActionCm / 100));
500    const actionText = this.getActionText(step);
501    const text = this.getPhaseText(phase, actionText, distanceMeters);
502    return {
503      step,
504      text,
505      distanceToActionCm,
506      phase,
507      voiceCue: this.getVoiceCue(step, phase, distanceMeters)
508    };
509  }
510  presentFullListStep(step) {
511    if (step.type === DirectionType.Continue && step.distanceCm > 0) {
512      return this.translateService.instant("navigation.directions.continue_for", {
513        distance: Math.max(1, Math.round(step.distanceCm / 100))
514      });
515    }
516    return this.getActionText(step);
517  }
518  getActionText(step) {
519    switch (step.type) {
520      case DirectionType.Start:
521        return this.translateService.instant("navigation.directions.start");
522      case DirectionType.Continue:
523        return this.translateService.instant("navigation.directions.continue");
524      case DirectionType.TurnLeft:
525        return this.translateService.instant("navigation.directions.turn_left");
526      case DirectionType.TurnRight:
527        return this.translateService.instant("navigation.directions.turn_right");
528      case DirectionType.SlightLeft:
529        return this.translateService.instant("navigation.directions.bear_left");
530      case DirectionType.SlightRight:
531        return this.translateService.instant("navigation.directions.bear_right");
532      case DirectionType.EnterDoor:
533      case DirectionType.ExitDoor:
534        return this.translateService.instant("navigation.directions.go_through_door");
535      case DirectionType.Arrive:
536        return this.translateService.instant("navigation.directions.you_have_arrived");
537      case DirectionType.ExitBuilding:
538        return this.translateService.instant("navigation.directions.exit_building");
539      case DirectionType.EnterBuilding:
540        return this.translateService.instant("navigation.directions.enter_building");
541      case DirectionType.TakeStairs:
542      case DirectionType.TakeStairsUp:
543        return this.translateService.instant("navigation.directions.take_stairs_up", {
544          floor: this.getFloorName(step.destinationFloorLevel)
545        });
546      case DirectionType.TakeStairsDown:
547        return this.translateService.instant("navigation.directions.take_stairs_down", {
548          floor: this.getFloorName(step.destinationFloorLevel)
549        });
550      case DirectionType.TakeElevator:
551      case DirectionType.TakeElevatorUp:
552        return this.translateService.instant("navigation.directions.take_elevator_up", {
553          floor: this.getFloorName(step.destinationFloorLevel)
554        });
555      case DirectionType.TakeElevatorDown:
556        return this.translateService.instant("navigation.directions.take_elevator_down", {
557          floor: this.getFloorName(step.destinationFloorLevel)
558        });
559      default:
560        return this.translateService.instant("navigation.directions.continue");
561    }
562  }
563  getPhaseText(phase, actionText, distanceMeters) {
564    if (phase === "continue-distance") {
565      return this.translateService.instant("navigation.directions.continue_for", {
566        distance: Math.max(1, distanceMeters)
567      });
568    }
569    if (phase === "approach-distance") {
570      return this.translateService.instant("navigation.directions.in_distance", {
571        distance: Math.max(1, distanceMeters),
572        action: actionText
573      });
574    }
575    return actionText;
576  }
577  getVoiceCue(step, phase, distanceMeters) {
578    return {
579      phase,
580      distanceMeters: phase === "action" ? void 0 : distanceMeters,
581      actionType: step.type,
582      destinationFloorLevel: step.destinationFloorLevel
583    };
584  }
585  getFloorName(level) {
586    if (level === void 0 || level === null) {
587      return "";
588    }
589    if (level === 1) {
590      return this.translateService.instant("navigation.directions.floor_names_accusative.ground_floor");
591    }
592    if (level > 1) {
593      return this.translateService.instant("navigation.directions.floor_names_accusative.floor", { level });
594    }
595    return this.translateService.instant("navigation.directions.floor_names_accusative.level", {
596      level: Math.abs(level)
597    });
598  }
599};
600_NavigationInstructionPresenterService.\u0275fac = function NavigationInstructionPresenterService_Factory(__ngFactoryType__) {
601  return new (__ngFactoryType__ || _NavigationInstructionPresenterService)(\u0275\u0275inject(TranslateService));
602};
603_NavigationInstructionPresenterService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _NavigationInstructionPresenterService, factory: _NavigationInstructionPresenterService.\u0275fac, providedIn: "root" });
604var NavigationInstructionPresenterService = _NavigationInstructionPresenterService;
605(() => {
606  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NavigationInstructionPresenterService, [{
607    type: Injectable,
608    args: [{
609      providedIn: "root"
610    }]
611  }], () => [{ type: TranslateService }], null);
612})();
613
614// src/app/services/navigation-instructions/navigation-progress-instruction.service.ts
615var _NavigationProgressInstructionService = class _NavigationProgressInstructionService {
616  constructor(planner, presenter) {
617    this.planner = planner;
618    this.presenter = presenter;
619    this.config = DEFAULT_NAVIGATION_INSTRUCTION_CONFIG;
620  }
621  resolve(input) {
622    var _a, _b, _c;
623    const steps = this.planner.getSteps(input.route);
624    const nextAction = (_a = this.findArrivalStepIfReached(steps, input.distanceToDestinationCm)) != null ? _a : this.findNextActionStep(steps, input.distanceFromRouteStartCm);
625    const current = nextAction ? this.presenter.present(nextAction, this.getInstructionPhase(nextAction, input.distanceFromRouteStartCm, input.locationSource), Math.max(0, nextAction.routeDistanceCm - input.distanceFromRouteStartCm)) : this.createContinueToDestination(input, steps);
626    const excludedCurrentStepId = (current == null ? void 0 : current.phase) === "continue-distance" ? null : (_b = current == null ? void 0 : current.step.id) != null ? _b : null;
627    const upcoming = this.getUpcomingActions(steps, excludedCurrentStepId, input.distanceFromRouteStartCm);
628    const remainingStepCount = current ? steps.filter((step) => isActionInstruction(step.type) && step.routeDistanceCm >= current.step.routeDistanceCm).length : 0;
629    return {
630      current,
631      upcoming,
632      distanceToNextActionCm: (_c = current == null ? void 0 : current.distanceToActionCm) != null ? _c : input.distanceToDestinationCm,
633      remainingStepCount
634    };
635  }
636  toDirection(instruction, fallbackNode, remainingStepCount) {
637    var _a;
638    if (!instruction) {
639      return null;
640    }
641    const displayType = instruction.phase === "continue-distance" ? DirectionType.Continue : instruction.step.type;
642    return {
643      type: displayType,
644      instruction: instruction.text,
645      distance: instruction.distanceToActionCm,
646      node: (_a = instruction.step.node) != null ? _a : fallbackNode,
647      destinationFloorLevel: instruction.step.destinationFloorLevel,
648      voiceCue: instruction.voiceCue,
649      instructionStepId: instruction.step.id,
650      routeDistanceCm: instruction.step.routeDistanceCm,
651      distanceToActionCm: instruction.distanceToActionCm,
652      instructionPhase: instruction.phase,
653      remainingStepCount
654    };
655  }
656  toDirections(instructions) {
657    return instructions.map((instruction) => this.toDirection(instruction)).filter((direction) => direction !== null);
658  }
659  getAllDirections(route) {
660    return this.planner.getSteps(route).map((step) => ({
661      type: step.type,
662      instruction: this.presenter.presentFullListStep(step),
663      distance: step.distanceCm,
664      node: step.node,
665      destinationFloorLevel: step.destinationFloorLevel,
666      instructionStepId: step.id,
667      routeDistanceCm: step.routeDistanceCm,
668      distanceToActionCm: step.distanceCm,
669      instructionPhase: "action"
670    }));
671  }
672  findNextActionStep(steps, distanceFromRouteStartCm) {
673    var _a;
674    const actionSteps = steps.filter((step) => isActionInstruction(step.type));
675    const nextUnpassedStep = actionSteps.find((step) => step.routeDistanceCm >= distanceFromRouteStartCm);
676    if (nextUnpassedStep) {
677      return nextUnpassedStep;
678    }
679    return (_a = actionSteps.find((step) => step.routeDistanceCm >= distanceFromRouteStartCm - 100)) != null ? _a : null;
680  }
681  findArrivalStepIfReached(steps, distanceToDestinationCm) {
682    var _a;
683    if (distanceToDestinationCm > 0) {
684      return null;
685    }
686    return (_a = steps.find((step) => step.type === DirectionType.Arrive)) != null ? _a : null;
687  }
688  getInstructionPhase(step, distanceFromRouteStartCm, locationSource) {
689    const distanceToActionCm = Math.max(0, step.routeDistanceCm - distanceFromRouteStartCm);
690    const farThreshold = this.getFarThreshold(locationSource);
691    const approachThreshold = this.getApproachThreshold(locationSource);
692    if (step.type === DirectionType.Arrive) {
693      return distanceToActionCm <= approachThreshold ? "action" : "continue-distance";
694    }
695    if (distanceToActionCm <= approachThreshold) {
696      return "action";
697    }
698    if (distanceToActionCm > farThreshold) {
699      return "continue-distance";
700    }
701    return "approach-distance";
702  }
703  getUpcomingActions(steps, currentStepId, distanceFromRouteStartCm) {
704    return steps.filter((step) => isActionInstruction(step.type)).filter((step) => step.id !== currentStepId).filter((step) => step.routeDistanceCm >= distanceFromRouteStartCm).slice(0, this.config.upcomingInstructionLimit).map((step) => this.presenter.present(step, "action", Math.max(0, step.routeDistanceCm - distanceFromRouteStartCm)));
705  }
706  createContinueToDestination(input, steps) {
707    var _a;
708    const arrivalStep = (_a = steps.find((step) => step.type === DirectionType.Arrive)) != null ? _a : steps[steps.length - 1];
709    if (!arrivalStep) {
710      return null;
711    }
712    return this.presenter.present(__spreadProps(__spreadValues({}, arrivalStep), {
713      type: DirectionType.Continue,
714      distanceCm: input.distanceToDestinationCm
715    }), "continue-distance", input.distanceToDestinationCm);
716  }
717  getFarThreshold(locationSource) {
718    return locationSource === LocationSource.GPS ? this.config.outdoorFarThresholdCm : this.config.indoorFarThresholdCm;
719  }
720  getApproachThreshold(locationSource) {
721    return locationSource === LocationSource.GPS ? this.config.outdoorApproachThresholdCm : this.config.indoorApproachThresholdCm;
722  }
723};
724_NavigationProgressInstructionService.\u0275fac = function NavigationProgressInstructionService_Factory(__ngFactoryType__) {
725  return new (__ngFactoryType__ || _NavigationProgressInstructionService)(\u0275\u0275inject(RouteInstructionPlannerService), \u0275\u0275inject(NavigationInstructionPresenterService));
726};
727_NavigationProgressInstructionService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _NavigationProgressInstructionService, factory: _NavigationProgressInstructionService.\u0275fac, providedIn: "root" });
728var NavigationProgressInstructionService = _NavigationProgressInstructionService;
729(() => {
730  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NavigationProgressInstructionService, [{
731    type: Injectable,
732    args: [{
733      providedIn: "root"
734    }]
735  }], () => [{ type: RouteInstructionPlannerService }, { type: NavigationInstructionPresenterService }], null);
736})();
737
738// src/app/services/navigation/navigation.service.ts
739var OUTDOOR_ROUTE_START_CANDIDATE_RADIUS_CM = 3e3;
740var MAX_OUTDOOR_ROUTE_START_CANDIDATES = 8;
741var NavigationStartError = class extends Error {
742  constructor(code, message) {
743    super(message);
744    this.code = code;
745    this.name = "NavigationStartError";
746  }
747};
748var RouteCalculationSuspendedError = class extends Error {
749  constructor() {
750    super("Route calculation suspended until outdoor GPS is available");
751    this.name = "RouteCalculationSuspendedError";
752  }
753};
754var _NavigationService = class _NavigationService {
755  // (2 cm)²
756  /** Renderer-safe signal; the route-coordinate context itself remains private. */
757  get externalRouteProjectionActive$() {
758    var _a;
759    (_a = this.externalRouteProjectionActiveSubject) != null ? _a : this.externalRouteProjectionActiveSubject = new BehaviorSubject(!!this.activeExternalRouteCoordinateContext);
760    return this.externalRouteProjectionActiveSubject.asObservable();
761  }
762  /** Some focused specs construct the service prototype directly; keep diagnostics available there too. */
763  get diagnosticTimeline() {
764    var _a;
765    return (_a = this.diagnostics) != null ? _a : this.diagnostics = new NavigationDiagnosticsService();
766  }
767  constructor(locationService, databaseService, alertController, translateService, settingsService, navigationStateManager, progressInstructionService, campusConfigurationService, debugControlService, diagnostics = new NavigationDiagnosticsService()) {
768    this.locationService = locationService;
769    this.databaseService = databaseService;
770    this.alertController = alertController;
771    this.translateService = translateService;
772    this.settingsService = settingsService;
773    this.navigationStateManager = navigationStateManager;
774    this.progressInstructionService = progressInstructionService;
775    this.campusConfigurationService = campusConfigurationService;
776    this.debugControlService = debugControlService;
777    this.diagnostics = diagnostics;
778    this.debugPenalties = null;
779    this.allowOutsideCampusRouteStart = false;
780    this.navigationGeneration = 0;
781    this.destroy$ = new Subject();
782    this.navigationState$ = new BehaviorSubject({
783      mode: NavigationMode.Idle,
784      target: null,
785      destinationNode: null,
786      route: null,
787      currentDirection: null,
788      nextDirections: [],
789      currentSegmentIndex: 0,
790      currentPosition: null,
791      distanceToNextTurn: 0,
792      distanceToDestination: 0,
793      distanceFromRouteStart: 0,
794      estimatedTimeRemaining: 0,
795      isOffRoute: false,
796      isNearingOffRoute: false,
797      recalculating: false,
798      positionStatus: "unavailable"
799    });
800    this.navigationEvent$ = new Subject();
801    this.isNavigating$ = this.navigationState$.pipe(map((state) => state.mode === NavigationMode.Navigating || state.mode === NavigationMode.Arrived), distinctUntilChanged(), shareReplay(1));
802    this.isOffRoute$ = this.navigationState$.pipe(map((state) => state.isOffRoute), distinctUntilChanged(), shareReplay(1));
803    this.isNearingOffRoute$ = this.navigationState$.pipe(map((state) => state.isNearingOffRoute), distinctUntilChanged(), shareReplay(1));
804    this.isRecalculating$ = this.navigationState$.pipe(map((state) => state.recalculating), distinctUntilChanged(), shareReplay(1));
805    this.route$ = this.navigationState$.pipe(map((state) => state.route), distinctUntilChanged(), shareReplay(1));
806    this.currentDirection$ = this.navigationState$.pipe(map((state) => state.currentDirection), distinctUntilChanged(), shareReplay(1));
807    this.distanceToDestination$ = this.navigationState$.pipe(map((state) => state.distanceToDestination), distinctUntilChanged(), shareReplay(1));
808    this.distanceFromRouteStart$ = this.navigationState$.pipe(map((state) => state.distanceFromRouteStart), distinctUntilChanged(), shareReplay(1));
809    this.navigationHeading$ = this.navigationState$.pipe(map((state) => {
810      if (state.mode !== NavigationMode.Navigating || state.isApproachingCampus || !state.route || !state.currentPosition) {
811        return null;
812      }
813      const currentSegment = state.route.segments[state.currentSegmentIndex];
814      if (!currentSegment) {
815        return null;
816      }
817      const headingSegment = this.resolveNavigationHeadingSegment(state.route, state.currentSegmentIndex, state.currentPosition);
818      const nextWaypoint = {
819        x: headingSegment.endNode.entity.locationX || 0,
820        y: headingSegment.endNode.entity.locationY || 0
821      };
822      return this.calculateNavigationHeading(state.currentPosition, nextWaypoint, headingSegment);
823    }), distinctUntilChanged(), shareReplay(1));
824    this.WALKING_SPEED_MS = 1.4;
825    this.ARRIVAL_THRESHOLD = 300;
826    this.MIN_GPS_ARRIVAL_CONFIDENCE = 0.5;
827    this.ARRIVAL_ON_ROUTE_COMPLETION_ENABLED = true;
828    this.ROUTE_COMPLETION_MAX_REMAINING_CM = 2500;
829    this.ROUTE_COMPLETION_MAX_OFF_ROUTE_CM = 2500;
830    this.ROUTE_COMPLETION_STABLE_SAMPLES = 12;
831    this.ROUTE_COMPLETION_MIN_STABLE_WINDOW_MS = 5e3;
832    this.ROUTE_COMPLETION_MAX_STABLE_SPREAD_CM = 300;
833    this.routeEndArrivalSamples = [];
834    this.MAX_RECALCULATION_INTERVAL = 1e3;
835    this.SIGNIFICANT_MOVEMENT_THRESHOLD = 500;
836    this.INDOOR_OFF_ROUTE_THRESHOLD = 2e3;
837    this.INDOOR_SNAP_THRESHOLD = 500;
838    this.INDOOR_OFF_ROUTE_RECALC_INTERVAL = 2e4;
839    this.OUTDOOR_OFF_ROUTE_THRESHOLD = 600;
840    this.OUTDOOR_WARNING_THRESHOLD = 400;
841    this.OUTDOOR_SNAP_THRESHOLD = 500;
842    this.OUTDOOR_OFF_ROUTE_RECALC_INTERVAL = 3e4;
843    this.lastRecalculationTime = 0;
844    this.lastOffRouteDistance = 0;
845    this.currentLocationConfidence = 0;
846    this.currentLocationAccuracyCm = 0;
847    this.currentLocationRecordedAtMs = 0;
848    this.locationUpdateSequence = 0;
849    this.navigationStartLocationSequence = 0;
850    this.lastProgressLocationSequence = 0;
851    this.routeSuspendedForOutdoorGps = false;
852    this.initializeLocationTracking();
853    this.initializePeriodicUpdates();
854    this.initializeLanguageChangeListener();
855  }
856  /**
857   * Get off-route threshold based on location source
858   * Outdoor (GPS): 6m - triggers recalculation once the fix clearly leaves the route corridor
859   * Indoor (Beacon): 20m - tighter threshold for more accurate positioning
860   */
861  getOffRouteThreshold() {
862    return this.currentLocationSource === LocationSource.GPS ? this.OUTDOOR_OFF_ROUTE_THRESHOLD : this.INDOOR_OFF_ROUTE_THRESHOLD;
863  }
864  /**
865   * Get warning threshold based on location source
866   * Outdoor (GPS): 4m - early warning before off-route
867   * Indoor (Beacon): Not applicable
868   */
869  getWarningThreshold() {
870    return this.currentLocationSource === LocationSource.GPS ? this.OUTDOOR_WARNING_THRESHOLD : this.INDOOR_OFF_ROUTE_THRESHOLD;
871  }
872  /**
873   * Get snap threshold based on location source
874   * Outdoor (GPS): 5m - tolerates GPS jitter while staying below the off-route threshold
875   * Indoor (Beacon): 5m - tighter snapping for accurate positioning
876   */
877  getSnapThreshold() {
878    return this.currentLocationSource === LocationSource.GPS ? this.OUTDOOR_SNAP_THRESHOLD : this.INDOOR_SNAP_THRESHOLD;
879  }
880  /**
881   * Get periodic recalculation interval based on location source
882   * Outdoor (GPS): 30s - less frequent due to slower outdoor walking
883   * Indoor (Beacon): 20s - more frequent for indoor navigation
884   */
885  getPeriodicRecalcInterval() {
886    return this.currentLocationSource === LocationSource.GPS ? this.OUTDOOR_OFF_ROUTE_RECALC_INTERVAL : this.INDOOR_OFF_ROUTE_RECALC_INTERVAL;
887  }
888  /**
889   * Get observable navigation state
890   */
891  get state$() {
892    return this.navigationState$.asObservable();
893  }
894  /**
895   * Get observable navigation events
896   */
897  get events$() {
898    return this.navigationEvent$.asObservable();
899  }
900  /**
901   * Get current navigation state
902   */
903  get currentState() {
904    return this.navigationState$.value;
905  }
906  /**
907   * Preview navigation route without starting
908   */
909  previewNavigation(target, destinationNode, preferStairs = true, preferElevators = true) {
910    return __async(this, null, function* () {
911      if (!destinationNode || !this.currentUserPosition || !this.refreshPositionStatus())
912        return null;
913      return (yield this.calculateRoute(this.createRouteCalculationRequest(target, destinationNode, preferStairs, preferElevators))).route;
914    });
915  }
916  createRouteCalculationRequest(target, destinationNode, preferStairs, preferElevators, requireCampusEntranceStart = false) {
917    var _a;
918    return {
919      location: {
920        point: __spreadValues({}, this.currentUserPosition),
921        floor: this.currentUserFloor,
922        campus: this.currentUserCampus,
923        source: this.currentLocationSource,
924        geographicPoint: this.currentLocationGeographicPoint ? __spreadValues({}, this.currentLocationGeographicPoint) : void 0
925      },
926      target,
927      destinationNode,
928      preferStairs,
929      preferElevators,
930      options: {
931        allowOutsideCampusRouteStart: this.allowOutsideCampusRouteStart || this.shouldAllowOutsideCampusRouteStart(),
932        requireCampusEntranceStart
933      },
934      isPositionOverride: ((_a = this.debugControlService) == null ? void 0 : _a.isOverrideActive) === true
935    };
936  }
937  /** Calculates geometry only;
937 callers own guidance, throttling and result publication. */
938  calculateRoute(request) {
939    return __async(this, null, function* () {
940      var _a;
941      const result = {
942        route: null,
943        errorCode: null,
944        coordinateContext: null,
945        nearestNode: null,
946        path: []
947      };
948      try {
949        if (!Number.isFinite(request.location.point.x) || !Number.isFinite(request.location.point.y)) {
950          return __spreadProps(__spreadValues({}, result), { errorCode: "position-unavailable" });
951        }
952        const context = this.resolveRouteCoordinateContext(request.destinationNode, false, request);
953        result.coordinateContext = context;
954        if (!context)
955          return __spreadProps(__spreadValues({}, result), { errorCode: "start-node-unavailable" });
956        const options = this.getRouteStartSearchOptions(request.destinationNode, ((_a = request.options) == null ? void 0 : _a.requireCampusEntranceStart) === true || context.usesDestinationCampusProjection, request);
957        const scaleCache = /* @__PURE__ */ new Map();
958        const resolved = yield this.resolveRouteStart(context, options, request.destinationNode, request.preferStairs, request.preferElevators, void 0, scaleCache);
959        result.nearestNode = resolved.nearestNode;
960        result.path = resolved.path;
961        if (!resolved.nearestNode)
962          return __spreadProps(__spreadValues({}, result), {
963            errorCode: options.requireCampusEntranceStart ? "campus-entrance-unavailable" : "start-node
963-unavailable"
964          });
965        if (!resolved.path.length)
966          return __spreadProps(__spreadValues({}, result), { errorCode: "no-path" });
967        result.route = this.createNavigationRoute(resolved.path, context.position, context.usesDestinationCampusProjection ? context.geographicPoint : void 0, context.usesDestinationCampusProjection, scaleCache);
968        return result;
969      } catch (error) {
970        console.error("[NavigationService] Route calculation failed:", error);
971        return __spreadProps(__spreadValues({}, result), { errorCode: "calculation-failed" });
972      }
973    });
974  }
975  /**
976   * Calculate route preview with detailed information for navigation confirmation
977   */
978  calculateRoutePreview(target, destinationNode, preferStairs, preferElevators) {
979    return __async(this, null, function* () {
980      var _a, _b;
981      console.log(`[NavigationService] Calculating route preview with preferences:`, {
982        preferStairs,
983        preferElevators
984      });
985      if (!this.currentUserPosition || !this.refreshPositionStatus()) {
986        console.log("[NavigationService] No user position available");
987        return {
988          route: null,
989          errorCode: "position-unavailable",
990          hasFloorChange: false,
991          hasBuildingChange: false,
992          usesStairs: false,
993          usesElevators: false,
994          estimatedDistance: 0,
995          estimatedTime: 0
996        };
997      }
998      try {
999        const result = yield this.calculateRoute(this.createRouteCalculationRequest(target, destinationNode, preferStairs, preferElevators));
1000        const { route, path, coordinateContext } = result;
1001        if (!route || !coordinateContext)
1002          return {
1003            route: null,
1004            errorCode: result.errorCode,
1005            hasFloorChange: false,
1006            hasBuildingChange: false,
1007            usesStairs: false,
1008            usesElevators: false,
1009            estimatedDistance: 0,
1010            estimatedTime: 0
1011          };
1012        const floorIds = /* @__PURE__ */ new Set();
1013        for (const node of path) {
1014          if (node.entity.floorId) {
1015            floorIds.add(node.entity.floorId);
1016          }
1017        }
1018        const floors = Array.from(floorIds);
1019        const hasFloorChange = floors.length > 1;
1020        const usesStairs = route.directions.some((d) => d.type === DirectionType.TakeStairs || d.type === DirectionType.TakeStairsUp || d.type === DirectionType.TakeStairsDown);
1021        const usesElevators = route.directions.some((d) => d.type === DirectionType.TakeElevator || d.type === DirectionType.TakeElevatorUp || d.type === DirectionType.TakeElevatorDown);
1022        const buildings = /* @__PURE__ */ new Set();
1023        for (const node of path) {
1024          const floor = this.databaseService.Models.get(node.entity.floorId);
1025          if (floor == null ? void 0 : floor.entity.buildingId) {
1026            buildings.add(floor.entity.buildingId);
1027          }
1028        }
1029        const hasBuildingChange = buildings.size > 1;
1030        let fromFloor;
1031        let toFloor;
1032        if (hasFloorChange && floors.length >= 2) {
1033          const firstFloor = this.databaseService.Models.get(floors[0]);
1034          const lastFloor = this.databaseService.Models.get(floors[floors.length - 1]);
1035          fromFloor = (_a = firstFloor == null ? void 0 : firstFloor.entity.level) == null ? void 0 : _a.toString();
1036          toFloor = (_b = lastFloor == null ? void 0 : lastFloor.entity.level) == null ? void 0 : _b.toString();
1037        }
1038        let fromBuilding;
1039        let toBuilding;
1040        if (hasBuildingChange) {
1041          const buildingIds = Array.from(buildings);
1042          const firstBuilding = this.databaseService.Models.get(buildingIds[0]);
1043          const lastBuilding = this.databaseService.Models.get(buildingIds[buildingIds.length - 1]);
1044          fromBuilding = firstBuilding == null ? void 0 : firstBuilding.entity.name;
1045          toBuilding = lastBuilding == null ? void 0 : lastBuilding.entity.name;
1046        }
1047        const estimatedDistance = route.totalDistance / 100;
1048        const estimatedTime = estimatedDistance / this.WALKING_SPEED_MS;
1049        return {
1050          route,
1051          errorCode: null,
1052          isCampusApproach: coordinateContext.usesDestinationCampusProjection,
1053          hasFloorChange,
1054          hasBuildingChange,
1055          usesStairs,
1056          usesElevators,
1057          estimatedDistance,
1058          estimatedTime,
1059          fromFloor,
1060          toFloor,
1061          fromBuilding,
1062          toBuilding
1063        };
1064      } catch (error) {
1065        console.error(`[NavigationService] Error calculating route preview:`, error);
1066        return {
1067          route: null,
1068          errorCode: "calculation-failed",
1069          hasFloorChange: false,
1070          hasBuildingChange: false,
1071          usesStairs: false,
1072          usesElevators: false,
1073          estimatedDistance: 0,
1074          estimatedTime: 0
1075        };
1076      }
1077    });
1078  }
1079  /**
1080   * Start navigation to a room
1081   */
1082  navigateToRoom(roomId, skipPreview = false) {
1083    return __async(this, null, function* () {
1084      console.log(`[NavigationService] Starting navigation to room: ${roomId}`);
1085      const room = this.databaseService.Models.get(roomId);
1086      if (!room) {
1087        throw new Error(`Room ${roomId} not found`);
1088      }
1089      const destinationNode = this.getDestinationNodeForRoom(room);
1090      const target = {
1091        type: "room",
1092        id: roomId,
1093        name: room.entity.name || "Unknown Room",
1094        floorId: room.entity.floorId,
1095        roomId
1096      };
1097      console.log(`[NavigationService] Calling startNavigation with destinationNode:`, destinationNode ? "present" : "undefined");
1098      yield this.startNavigation(target, destinationNode, true, true, skipPreview);
1099    });
1100  }
1101  /**
1102   * Get the destination node for a room, with fallback to nearest node
1103   */
1104  getDestinationNodeForRoom(room) {
1105    console.log(`[NavigationService] Getting destination node for room:`, {
1106      roomId: room.entity.id,
1107      roomName: room.entity.name,
1108      roomNodeId: room.entity.nodeId,
1109      roomNode: room.node ? {
1110        nodeId: room.node.entity.id,
1111        nodeType: room.node.entity.type,
1112        nodeFloorId: room.node.entity.floorId,
1113        nodeLocationX: room.node.entity.locationX,
1114        nodeLocationY: room.node.entity.locationY
1115      } : null,
1116      floorId: room.entity.floorId
1117    });
1118    if (room.entity.nodeId && !room.node) {
1119      console.log(`[NavigationService] Room has nodeId (${room.entity.nodeId}) but node relation is not populated`);
1120      const manualNode = this.databaseService.Models.get(room.entity.nodeId);
1121      console.log(`[NavigationService] Manual node lookup result:`, manualNode ? {
1122        nodeId: manualNode.entity.id,
1123        nodeType: manualNode.entity.type,
1124        nodeLocationX: manualNode.entity.locationX,
1125        nodeLocationY: manualNode.entity.locationY,
1126        nodeExists: true
1127      } : { nodeExists: false });
1128      const allModelKeys = Array.from(this.databaseService.Models.keys());
1129      const nodeKeys = allModelKeys.filter((key) => key.startsWith("Node:") || key.includes("node"));
1130      console.log(`[NavigationService] Database contains ${allModelKeys.length} total models, ${nodeKeys.length} node-related keys:`, nodeKeys.slice(0, 5));
1131    } else if (!room.entity.nodeId) {
1132      console.log(`[NavigationService] Room ${room.entity.id} has no nodeId in entity - attempting fallback to nearest node`);
1133    }
1134    let destinationNode = room.node;
1135    if (!destinationNode && room.entity.floorId) {
1136      const roomCenter = this.calculateRoomCenter(room);
1137      if (roomCenter) {
1138        console.log(`[NavigationService] Calculated room center:`, roomCenter);
1139        const nearestNode = this.findNearestNode(roomCenter, room.entity.floorId);
1140        destinationNode = nearestNode || void 0;
1141        if (destinationNode) {
1142          console.log(`[NavigationService] Found nearest node fallback:`, {
1143            nodeId: destinationNode.entity.id,
1144            nodeType: destinationNode.entity.type,
1145            nodeLocationX: destinationNode.entity.locationX,
1146            nodeLocationY: destinationNode.entity.locationY,
1147            distanceFromRoom: MathHelpers.calculateDistance(roomCenter, {
1148              x: destinationNode.entity.locationX || 0,
1149              y: destinationNode.entity.locationY || 0
1150            })
1151          });
1152        } else {
1153          console.log(`[NavigationService] No nodes found on floor ${room.entity.floorId}`);
1154        }
1155      } else {
1156        console.log(`[NavigationService] Could not calculate room center - room has no corners`);
1157      }
1158    }
1159    return destinationNode;
1160  }
1161  /**
1162   * Calculate the center point of a room based on its corners
1163   */
1164  calculateRoomCenter(room) {
1165    if (!room.corners || room.corners.length === 0) {
1166      return null;
1167    }
1168    let sumX = 0;
1169    let sumY = 0;
1170    let validCorners = 0;
1171    for (const corner of room.corners) {
1172      if (corner.entity.locationX !== void 0 && corner.entity.locationY !== void 0) {
1173        sumX += corner.entity.locationX;
1174        sumY += corner.entity.locationY;
1175        validCorners++;
1176      }
1177    }
1178    if (validCorners === 0) {
1179      return null;
1180    }
1181    return {
1182      x: sumX / validCorners,
1183      y: sumY / validCorners
1184    };
1185  }
1186  /**
1187   * Resolve destination for a person
1188   * Handles room selection if person has multiple rooms
1189   * @returns Promise with person, room, and destination node
1190   * @throws Error if person not found, has no rooms, or room/node cannot be resolved
1191   */
1192  resolvePersonDestination(personId) {
1193    return __async(this, null, function* () {
1194      var _a, _b;
1195      console.log(`[NavigationService] Resolving destination for person: ${personId}`);
1196      const person = this.databaseService.Models.get(personId);
1197      if (!person) {
1198        console.error(`[NavigationService] Person ${personId} not found in database`);
1199        throw new Error(`Person not found`);
1200      }
1201      console.log(`[NavigationService] Person found:`, {
1202        personId: person.entity.id,
1203        personName: person.entity.name,
1204        personRoomsCount: ((_a = person.personRooms) == null ? void 0 : _a.length) || 0,
1205        personRooms: (_b = person.personRooms) == null ? void 0 : _b.map((pr) => ({
1206          roomId: pr.entity.roomId,
1207          isPrimary: pr.entity.isPrimary
1208        }))
1209      });
1210      if (!person.personRooms || person.personRooms.length === 0) {
1211        console.error(`[NavigationService] Person ${person.entity.name} has no associated rooms`);
1212        throw new Error(`${person.entity.name} has no assigned office`);
1213      }
1214      let selectedPersonRoom;
1215      if (person.personRooms.length === 1) {
1216        selectedPersonRoom = person.personRooms[0];
1217        console.log(`[NavigationService] Person has single room:`, {
1218          roomId: selectedPersonRoom.entity.roomId
1219        });
1220      } else {
1221        console.log(`[NavigationService] Person has ${person.personRooms.length} rooms, prompting user to select`);
1222        selectedPersonRoom = yield this.promptRoomSelection(person);
1223        if (!selectedPersonRoom) {
1224          console.log(`[NavigationService] User cancelled room selection`);
1225          throw new Error("Room selection cancelled");
1226        }
1227        console.log(`[NavigationService] User selected room:`, {
1228          roomId: selectedPersonRoom.entity.roomId
1229        });
1230      }
1231      const room = this.databaseService.Models.get(selectedPersonRoom.entity.roomId);
1232      if (!room) {
1233        console.error(`[NavigationService] Room ${selectedPersonRoom.entity.roomId} not found in database`);
1234        throw new Error(`Room not found`);
1235      }
1236      console.log(`[NavigationService] Room loaded:`, {
1237        roomId: room.entity.id,
1238        roomName: room.entity.name,
1239        floorId: room.entity.floorId,
1240        nodeId: room.entity.nodeId,
1241        hasNodeRelation: !!room.node
1242      });
1243      const destinationNode = this.getDestinationNodeForRoom(room);
1244      if (!destinationNode) {
1245        console.error(`[NavigationService] Could not find destination node for room ${room.entity.id}`);
1246        throw new Error(`Cannot find route to this location`);
1247      }
1248      console.log(`[NavigationService] Destination resolved:`, {
1249        nodeId: destinationNode.entity.id,
1250        nodeType: destinationNode.entity.type,
1251        nodeFloorId: destinationNode.entity.floorId
1252      });
1253      return { person, room, destinationNode };
1254    });
1255  }
1256  /**
1257   * Start navigation to a person's location
1258   */
1259  navigateToPerson(personId, skipPreview = false) {
1260    return __async(this, null, function* () {
1261      console.log(`[NavigationService] Starting navigation to person: ${personId}`);
1262      const { person, room, destinationNode } = yield this.resolvePersonDestination(personId);
1263      const target = {
1264        type: "person",
1265        id: personId,
1266        name: person.entity.name || "Unknown Person",
1267        floorId: room.entity.floorId,
1268        roomId: room.entity.id
1269      };
1270      console.log(`[NavigationService] Navigation target created:`, target);
1271      console.log(`[NavigationService] About to call startNavigation`);
1272      try {
1273        yield this.startNavigation(target, destinationNode, true, true, skipPreview);
1274        console.log(`[NavigationService] startNavigation completed successfully`);
1275      } catch (error) {
1276        console.error(`[NavigationService] startNavigation failed:`, error);
1277        throw error;
1278      }
1279    });
1280  }
1281  /**
1282   * Prompt user to select a room from multiple options
1283   * Shows room name, floor, building, and marks primary room
1284   */
1285  promptRoomSelection(person) {
1286    return __async(this, null, function* () {
1287      return new Promise((resolve) => __async(this, null, function* () {
1288        var _a;
1289        const alert = yield this.alertController.create({
1290          header: yield this.translateService.get("navigation.room_selection.title", {
1291            name: person.entity.name
1292          }).toPromise(),
1293          message: yield this.translateService.get("navigation.room_selection.message").toPromise(),
1294          inputs: ((_a = person.personRooms) == null ? void 0 : _a.map((personRoom, index) => {
1295            var _a2;
1296            const room = this.databaseService.Models.get(personRoom.entity.roomId);
1297            const roomName = (room == null ? void 0 : room.entity.name) || (room == null ? void 0 : room.entity.number) || (room == null ? void 0 : room.entity.description) || `Room ${(_a2 = personRoom.entity.roomId) == null ? void 0 : _a2.slice(-4)}`;
1298            const locationParts = [];
1299            if (room == null ? void 0 : room.entity.floorId) {
1300              const floor = this.databaseService.Models.get(room.entity.floorId);
1301              if (floor) {
1302                if (floor.entity.buildingId) {
1303                  const building = this.databaseService.Models.get(floor.entity.buildingId);
1304                  if (building == null ? void 0 : building.entity.name) {
1305                    locationParts.push(building.entity.name);
1306                  }
1307                }
1308                if (floor.entity.level !== null && floor.entity.level !== void 0) {
1309                  locationParts.push(`Floor ${floor.entity.level}`);
1310                }
1311              }
1312            }
1313            let label = roomName;
1314            if (locationParts.length > 0) {
1315              label += ` (${locationParts.join(", ")})`;
1316            }
1317            if (personRoom.entity.isPrimary) {
1318              label += " \u2B50";
1319            }
1320            return {
1321              type: "radio",
1322              label,
1323              value: personRoom,
1324              checked: personRoom.entity.isPrimary || index === 0
1325              // Select primary or first option by default
1326            };
1327          })) || [],
1328          buttons: [
1329            {
1330              text: yield this.translateService.get("common.cancel").toPromise(),
1331              role: "cancel",
1332              handler: () => {
1333                console.log("[NavigationService] Room selection cancelled");
1334                resolve(null);
1335              }
1336            },
1337            {
1338              text: yield this.translateService.get("common.navigate").toPromise(),
1339              handler: (selectedPersonRoom) => {
1340                console.log("[NavigationService] Room selected:", selectedPersonRoom);
1341                resolve(selectedPersonRoom);
1342              }
1343            }
1344          ]
1345        });
1346        yield alert.present();
1347      }));
1348    });
1349  }
1350  /**
1351   * Start navigation to a specific node
1352   */
1353  navigateToNode(nodeId, skipPreview = false) {
1354    return __async(this, null, function* () {
1355      const node = this.databaseService.Models.get(nodeId);
1356      if (!node) {
1357        throw new Error(`Node ${nodeId} not found`);
1358      }
1359      const target = {
1360        type: "node",
1361        id: nodeId,
1362        name: `Waypoint ${nodeId.slice(-4)}`,
1363        floorId: node.entity.floorId || void 0
1364      };
1365      yield this.startNavigation(target, node, true, true, skipPreview);
1366    });
1367  }
1368  /**
1369   * Cancel current navigation
1370   */
1371  cancelNavigation() {
1372    this.resetNavigation();
1373    this.navigationEvent$.next({
1374      type: "cancelled",
1375      state: this.currentState
1376    });
1377  }
1378  /**
1379   * Clear an arrived route without reporting it as cancelled.
1380   */
1381  completeNavigation() {
1382    this.resetNavigation();
1383  }
1384  resetNavigation() {
1385    var _a;
1386    this.navigationGeneration = ((_a = this.navigationGeneration) != null ? _a : 0) + 1;
1387    if (this.activeRouteCorrelationId) {
1388      this.diagnosticTimeline.record("route", "stopped", {
1389        previousMode: this.currentState.mode
1390      }, this.activeRouteCorrelationId);
1391    }
1392    this.activeRouteCorrelationId = void 0;
1393    this.clearRouteSuspension();
1394    this.setActiveExternalRouteCoordinateContext(void 0);
1395    this.allowOutsideCampusRouteStart = false;
1396    this.beginArrivalConfirmation();
1397    this.resetProgressContinuity();
1398    this.updateState({
1399      mode: NavigationMode.Idle,
1400      isApproachingCampus: false,
1401      target: null,
1402      destinationNode: null,
1403      route: null,
1404      currentDirection: null,
1405      nextDirections: [],
1406      currentSegmentIndex: 0,
1407      currentPosition: null,
1408      distanceToNextTurn: 0,
1409      distanceToDestination: 0,
1410      estimatedTimeRemaining: 0,
1411      isOffRoute: false,
1412      isNearingOffRoute: false,
1413      recalculating: false,
1414      positionStatus: this.getRawPositionStatus()
1415    });
1416    this.navigationStateManager.setDestinationRoomId(null);
1417  }
1418  /**
1419   * Recalculate route from current position
1420   */
1421  recalculateRoute() {
1422    return __async(this, null, function* () {
1423      var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
1424      const generation = (_a = this.navigationGeneration) != null ? _a : 0;
1425      const state = this.currentState;
1426      if (!state.target || state.mode === NavigationMode.Idle) {
1427        return;
1428      }
1429      if (!this.refreshPositionStatus()) {
1430        return;
1431      }
1432      const now = Date.now();
1433      const timeSinceLastRecalc = now - this.lastRecalculationTime;
1434      if (timeSinceLastRecalc < this.MAX_RECALCULATION_INTERVAL) {
1435        return;
1436      }
1437      this.lastRecalculationTime = now;
1438      const destinationNode = state.destinationNode;
1439      if (!destinationNode) {
1440        console.error("[NavigationService] \u274C No destination node stored in navigation state for recalculation");
1441        return;
1442      }
1443      const coordinateContext = this.resolveRouteCoordinateContext(destinationNode);
1444      if (coordinateContext) {
1445        this.lastRecalculationPosition = __spreadValues({}, coordinateContext.position);
1446      }
1447      const correlationId = (_b = this.activeRouteCorrelationId) != null ? _b : this.diagnosticTimeline.nextCorrelationId("route");
1448      this.activeRouteCorrelationId = correlationId;
1449      this.diagnosticTimeline.record("route", "recalculation-requested", {
1450        destinationNodeId: (_c = destinationNode.entity.id) != null ? _c : "unknown",
1451        floorId: (_e = (_d = this.currentUserFloor) == null ? void 0 : _d.entity.id) != null ? _e : null,
1452        campusId: (_h = (_g = (_f = this.currentUserCampus) == null ? void 0 : _f.entity) == null ? void 0 : _g.id) != null ? _h : null,
1453        source: (_i = this.currentLocationSource) != null ? _i : "unknown"
1454      }, correlationId);
1455      this.updateState({ recalculating: true });
1456      this.navigationEvent$.next({
1457        type: "recalculating",
1458        state: this.currentState
1459      });
1460      try {
1461        const settings = this.settingsService.currentSettings;
1462        const preferStairs = (_j = settings.preferStairs) != null ? _j : true;
1463        const preferElevators = (_k = settings.preferElevators) != null ? _k : true;
1464        yield this.calculateAndSetRoute(destinationNode, preferStairs, preferElevators);
1465        if (generation !== ((_l = this.navigationGeneration) != null ? _l : 0))
1466          return;
1467        this.diagnosticTimeline.record("route", "recalculation-completed", {
1468          segmentCount: (_n = (_m = this.currentState.route) == null ? void 0 : _m.segments.length) != null ? _n : 0,
1469          distance: Math.round((_p = (_o = this.currentState.route) == null ? void 0 : _o.totalDistance) != null ? _p : 0)
1470        }, correlationId);
1471      } catch (error) {
1472        if (error instanceof RouteCalculationSuspendedError) {
1473          return;
1474        }
1475        this.diagnosticTimeline.record("route", "recalculation-failed", {
1476          reason: error instanceof Error ? error.message : String(error)
1477        }, correlationId);
1478        console.error("[NavigationService] \u274C Route recalculation failed:", error);
1479      } finally {
1480        if (generation === ((_q = this.navigationGeneration) != null ? _q : 0))
1481          this.updateState({ recalculating: false });
1482      }
1483    });
1484  }
1485  /**
1486   * Initialize location tracking for navigation updates
1487   */
1488  initializeLocationTracking() {
1489    this.locationService.location$.pipe(takeUntil(this.destroy$), distinctUntilChanged((prev, curr) => {
1490      var _a, _b;
1491      if (!prev || !curr)
1492        return prev === curr;
1493      return prev.point.x === curr.point.x && prev.point.y === curr.point.y && ((_a = prev.floor) == null ? void 0 : _a.entity.id) === ((_b = curr.floor) == null ? void 0 : _b.entity.id) && prev.recordedAt.getTime() === curr.recordedAt.getTime();
1494    })).subscribe((location) => {
1495      var _a, _b;
1496      if (!location) {
1497        if (this.currentState.mode !== NavigationMode.Idle) {
1498          this.diagnosticTimeline.record("route", "location-lost", void 0, this.activeRouteCorrelationId);
1499        }
1500        this.currentUserPosition = void 0;
1501        this.currentUserFloor = void 0;
1502        this.currentUserCampus = void 0;
1503        this.currentLocationSource = void 0;
1504        this.currentLocationConfidence = 0;
1505        this.currentLocationAccuracyCm = 0;
1506        this.currentLocationGeographicPoint = void 0;
1507        this.currentLocationRecordedAtMs = 0;
1508        this.resetProgressContinuity();
1509        this.updateState({
1510          positionStatus: this.routeSuspendedForOutdoorGps ? "waiting-for-outdoor-gps" : "unavailable"
1511        });
1512      } else {
1513        this.currentUserPosition = location.point;
1514        this.currentUserFloor = location.floor;
1515        this.currentUserCampus = location.campus;
1516        this.currentLocationSource = location.source;
1517        this.currentLocationConfidence = (_a = location.confidenceAdjusted) != null ? _a : location.confidence;
1518        this.currentLocationAccuracyCm = (_b = location.accuracy) != null ? _b : 0;
1519        this.currentLocationGeographicPoint = location.geographicPoint;
1520        this.currentLocationRecordedAtMs = location.recordedAt.getTime();
1521        this.locationUpdateSequence++;
1522        this.refreshPositionStatus();
1523        this.updateNavigationProgress();
1524      }
1525    });
1526  }
1527  /**
1528   * Initialize periodic navigation updates
1529   */
1530  initializePeriodicUpdates() {
1531    timer(0, 1e3).pipe(takeUntil(this.destroy$), filter(() => this.currentState.mode === NavigationMode.Navigating)).subscribe(() => {
1532      this.updateNavigationProgress();
1533    });
1534  }
1535  /** Navigation must not keep consuming the last known point after its source
1536   * has stopped. GPS is allowed slightly longer than indoor beacon fixes. */
1537  refreshPositionStatus() {
1538    const rawPositionStatus = this.getRawPositionStatus();
1539    if (this.shouldSuspendActiveExternalRoute()) {
1540      this.enterRouteSuspension();
1541      return false;
1542    }
1543    if (this.routeSuspendedForOutdoorGps) {
1544      if (rawPositionStatus !== "available" || !this.canResumeSuspendedRoute()) {
1545        if (this.currentState.positionStatus !== "waiting-for-outdoor-gps") {
1546          this.updateState({ positionStatus: "waiting-for-outdoor-gps" });
1547        }
1548        return false;
1549      }
1550      this.resumeSuspendedRoute();
1551    }
1552    if (this.currentState.positionStatus !== rawPositionStatus) {
1553      this.updateState({ positionStatus: rawPositionStatus });
1554    }
1555    return rawPositionStatus === "available";
1556  }
1557  getRawPositionStatus() {
1558    let positionStatus;
1559    if (!this.currentUserPosition || !this.currentLocationRecordedAtMs) {
1560      positionStatus = "unavailable";
1561    } else {
1562      const timeoutMs = this.currentLocationSource === LocationSource.GPS ? 5e3 : 3e3;
1563      positionStatus = Date.now() - this.currentLocationRecordedAtMs <= timeoutMs ? "available" : "stale";
1564    }
1565    return positionStatus;
1566  }
1567  shouldSuspendActiveExternalRoute() {
1568    if (this.currentState.mode !== NavigationMode.Navigating || !this.currentState.route || !this.currentUserFloor || !this.activeExternalRouteCoordinateContext) {
1569      return false;
1570    }
1571    const indoorCampusId = this.resolveCurrentIndoorCampusId();
1572    return !indoorCampusId || indoorCampusId !== this.activeExternalRouteCoordinateContext.routeCampusId;
1573  }
1574  resolveCurrentIndoorCampusId() {
1575    var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
1576    if (!this.currentUserFloor) {
1577      return null;
1578    }
1579    const directCampusId = (_j = (_g = (_d = (_b = this.currentUserFloor.entity.campusId) != null ? _b : (_a = this.currentUserFloor.campus) == null ? void 0 : _a.entity.id) != null ? _d : (_c = this.currentUserFloor.building) == null ? void 0 : _c.entity.campusId) != null ? _g : (_f = (_e = this.currentUserFloor.building) == null ? void 0 : _e.campus) == null ? void 0 : _f.entity.id) != null ? _j : (_i = (_h = this.currentUserCampus) == null ? void 0 : _h.entity) == null ? void 0 : _i.id;
1580    if (directCampusId) {
1581      return directCampusId;
1582    }
1583    const buildingId = this.currentUserFloor.entity.buildingId;
1584    const building = buildingId ? (_l = (_k = this.databaseService) == null ? void 0 : _k.Models) == null ? void 0 : _l.get(buildingId) : void 0;
1585    return (_q = (_p = (_m = building == null ? void 0 : building.entity) == null ? void 0 : _m.campusId) != null ? _p : (_o = (_n = building == null ? void 0 : building.campus) == null ? void 0 : _n.entity) == null ? void 0 : _o.id) != null ? _q : null;
1586  }
1587  canResumeSuspendedRoute() {
1588    var _a, _b, _c;
1589    const routeCampusId = (_a = this.activeExternalRouteCoordinateContext) == null ? void 0 : _a.routeCampusId;
1590    if (!routeCampusId) {
1591      return false;
1592    }
1593    if (this.currentUserFloor) {
1594      return this.resolveCurrentIndoorCampusId() === routeCampusId;
1595    }
1596    if (this.currentLocationSource !== LocationSource.GPS) {
1597      return false;
1598    }
1599    const geographicPoint = this.currentLocationGeographicPoint;
1600    const hasValidGeographicPoint = !!geographicPoint && Number.isFinite(geographicPoint.latitude) && Number.isFinite(geographicPoint.longitude);
1601    return hasValidGeographicPoint || ((_c = (_b = this.currentUserCampus) == null ? void 0 : _b.entity) == null ? void 0 : _c.id) === routeCampusId;
1602  }
1603  enterRouteSuspension() {
1604    var _a, _b, _c;
1605    if (!this.routeSuspendedForOutdoorGps) {
1606      this.routeSuspendedForOutdoorGps = true;
1607      this.diagnosticTimeline.record("route", "route-suspended", {
1608        reason: "indoor-at-incompatible-campus",
1609        indoorCampusId: this.resolveCurrentIndoorCampusId(),
1610        routeCampusId: (_b = (_a = this.activeExternalRouteCoordinateContext) == null ? void 0 : _a.routeCampusId) != null ? _b : null,
1611        source: (_c = this.currentLocationSource) != null ? _c : "unknown"
1612      }, this.activeRouteCorrelationId);
1613    }
1614    if (this.currentState.positionStatus !== "waiting-for-outdoor-gps" || this.currentState.isOffRoute || this.currentState.isNearingOffRoute || this.currentState.recalculating) {
1615      this.updateState({
1616        positionStatus: "waiting-for-outdoor-gps",
1617        isOffRoute: false,
1618        isNearingOffRoute: false,
1619        recalculating: false
1620      });
1621    }
1622  }
1623  resumeSuspendedRoute() {
1624    var _a, _b, _c, _d;
1625    this.routeSuspendedForOutdoorGps = false;
1626    this.diagnosticTimeline.record("route", "route-resumed", {
1627      campusId: this.currentUserFloor ? this.resolveCurrentIndoorCampusId() : (_c = (_b = (_a = this.currentUserCampus) == null ? void 0 : _a.entity) == null ? void 0 : _b.id) != null ? _c : null,
1628      source: (_d = this.currentLocationSource) != null ? _d : "unknown"
1629    }, this.activeRouteCorrelationId);
1630  }
1631  clearRouteSuspension() {
1632    this.routeSuspendedForOutdoorGps = false;
1633  }
1634  /**
1635   * Initialize language change listener to update navigation instructions
1636   */
1637  initializeLanguageChangeListener() {
1638    this.translateService.onLangChange.pipe(takeUntil(this.destroy$)).subscribe(() => {
1639      const state = this.currentState;
1640      if (state.route) {
1641        const instructionUpdate = this.resolveProgressInstructions(state.route, state.distanceFromRouteStart, state.distanceToDestination);
1642        this.updateState({
1643          currentDirection: instructionUpdate.currentDirection,
1644          nextDirections: instructionUpdate.nextDirections,
1645          distanceToNextTurn: instructionUpdate.distanceToNextTurn
1646        });
1647      }
1648    });
1649  }
1650  /**
1651   * Start navigation to a target
1652   */
1653  startNavigation(_0, _1) {
1654    return __async(this, arguments, function* (target, destinationNode, preferStairs = true, preferElevators = true, skipPreview = false, options = {}) {
1655      var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1656      const generation = this.navigationGeneration = ((_a = this.navigationGeneration) != null ? _a : 0) + 1;
1657      this.clearRouteSuspension();
1658      this.setActiveExternalRouteCoordinateContext(void 0);
1659      this.activeRouteCorrelationId = this.diagnosticTimeline.nextCorrelationId("route");
1660      this.diagnosticTimeline.record("route", "calculation-requested", {
1661        targetType: target.type,
1662        targetId: target.id,
1663        destinationNodeId: (_b = destinationNode == null ? void 0 : destinationNode.entity.id) != null ? _b : null,
1664        preferStairs,
1665        preferElevators,
1666        floorId: (_d = (_c = this.currentUserFloor) == null ? void 0 : _c.entity.id) != null ? _d : null,
1667        campusId: (_g = (_f = (_e = this.currentUserCampus) == null ? void 0 : _e.entity) == null ? void 0 : _f.id) != null ? _g : null
1668      }, this.activeRouteCorrelationId);
1669      console.log(`[NavigationService] startNavigation called with:`, {
1670        target,
1671        destinationNode: destinationNode ? {
1672          nodeId: destinationNode.entity.id,
1673          nodeType: destinationNode.entity.type,
1674          nodeLocationX: destinationNode.entity.locationX,
1675          nodeLocationY: destinationNode.entity.locationY
1676        } : null,
1677        preferStairs,
1678        preferElevators
1679      });
1680      if (!destinationNode) {
1681        console.error(`[NavigationService] CRITICAL: Destination node is undefined/null for target:`, target);
1682        console.error(`[NavigationService] This means the room has no direct node and n
1682o fallback node could be found`);
1683        throw new Error("Destination node not found");
1684      }
1685      if (!this.currentUserPosition || !this.refreshPositionStatus()) {
1686        throw new Error("User position not available");
1687      }
1688      this.allowOutsideCampusRouteStart = (_h = options.allowOutsideCampusRouteStart) != null ? _h : this.shouldAllowOutsideCampusRouteStart();
1689      console.log("[NavigationService] \u{1F680} Starting navigation - Setting isOffRoute: false, destinationNode:", destinationNode == null ? void 0 : destinationNode.entity.id);
1690      this.updateState({
1691        mode: NavigationMode.Idle,
1692        // Start with idle, will switch to navigating once route is calculated
1693        target,
1694        destinationNode,
1695        route: null,
1696        currentDirection: null,
1697        nextDirections: [],
1698        currentSegmentIndex: 0,
1699        currentPosition: null,
1700        distanceToNextTurn: 0,
1701        distanceToDestination: 0,
1702        distanceFromRouteStart: 0,
1703        estimatedTimeRemaining: 0,
1704        isOffRoute: false,
1705        isNearingOffRoute: false,
1706        recalculating: false
1707      });
1708      if (target.roomId) {
1709        console.log("[NavigationService] Setting destination room ID for highlighting:", target.roomId);
1710        this.navigationStateManager.setDestinationRoomId(target.roomId);
1711      }
1712      try {
1713        yield this.calculateAndSetRoute(destinationNode, preferStairs, preferElevators, 0, options.requireCampusEntranceStart === true, true);
1714      } catch (error) {
1715        if (generation !== this.navigationGeneration)
1716          return;
1717        this.resetNavigation();
1718        throw error;
1719      }
1720      if (generation !== this.navigationGeneration)
1721        return;
1722      this.beginArrivalConfirmation();
1723      console.log("[NavigationService] \u2705 Navigation ready - Route has", ((_i = this.currentState.route) == null ? void 0 : _i.segments.length) || 0, "segments");
1724      this.updateState({ mode: NavigationMode.Navigating });
1725      this.navigationEvent$.next({
1726        type: "started",
1727        state: this.currentState
1728      });
1729    });
1730  }
1731  /**
1732   * Calculate route and update state
1733   */
1734  calculateAndSetRoute(destinationNode, preferStairs = true, preferElevators = true, contextRetryCount = 0, requireCampusEntranceStart = false, isInitialRoute = false) {
1735    return __async(this, null, function* () {
1736      var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D;
1737      const generation = (_a = this.navigationGeneration) != null ? _a : 0;
1738      this.resetProgressContinuity();
1739      if (!this.currentUserPosition || !this.refreshPositionStatus()) {
1740        console.error("[NavigationService] \u274C User position not available for route calculation");
1741        throw new Error("User position not available");
1742      }
1743      const routeStartLocationSequence = this.locationUpdateSequence;
1744      const request = this.createRouteCalculationRequest((_b = this.currentState.target) != null ? _b : { type: "node", id: destinationNode.entity.id }, destinationNode, preferStairs, preferElevators, requireCampusEntranceStart);
1745      const result = yield this.calculateRoute(request);
1746      if (generation !== ((_c = this.navigationGeneration) != null ? _c : 0))
1747        return;
1748      if (this.shouldSuspendActiveExternalRoute()) {
1749        this.enterRouteSuspension();
1750        throw new RouteCalculationSuspendedError();
1751      }
1752      const { coordinateContext, nearestNode: startNode, path, route } = result;
1753      if (!coordinateContext || !startNode || !route) {
1754        if (result.errorCode === "campus-entrance-unavailable") {
1755          throw new NavigationStartError("campus-entrance-unavailable", "The destination campus has no configured outdoor entrance node.");
1756        }
1757        throw new Error(result.errorCode === "no-path" ? "No path found to destination" : result.errorCode === "start-node
1757-unavailable" ? "Could not find nearby navigation point" : "Route calculation failed");
1758      }
1759      const routeStartPosition = coordinateContext.position;
1760      const routeStartFloorId = (_d = coordinateContext.floor) == null ? void 0 : _d.entity.id;
1761      const routeStartCampusId = (_e = coordinateContext.searchCampus) == null ? void 0 : _e.entity.id;
1762      const entranceStartRequired = requireCampusEntranceStart || coordinateContext.usesDestinationCampusProjection;
1763      this.diagnosticTimeline.record("route", "nodes-selected", {
1764        startNodeId: (_h = (_g = (_f = path[0]) == null ? void 0 : _f.entity.id) != null ? _g : startNode.entity.id) != null ? _h : "unknown",
1765        startFloorId: (_j = ((_i = path[0]) != null ? _i : startNode).entity.floorId) != null ? _j : null,
1766        destinationNodeId: (_k = destinationNode.entity.id) != null ? _k : "unknown",
1767        destinationFloorId: (_l = destinationNode.entity.floorId) != null ? _l : null,
1768        campusId: routeStartCampusId != null ? routeStartCampusId : null,
1769        routeCampusId: (_n = (_m = coordinateContext.destinationCampus) == null ? void 0 : _m.entity.id) != null ? _n : null,
1770        coordinateSource: coordinateContext.usesDestinationCampusProjection ? "destination-campus-projection" : "physical-location",
1771        geoReferencePointCount: (_p = (_o = coordinateContext.transformQuality) == null ? void 0 : _o.referencePointCount) != null ? _p : null,
1772        geoReferenceReliable: (_r = (_q = coordinateContext.transformQuality) == null ? void 0 : _q.isReliable) != null ? _r : null,
1773        routeStartPolicy: entranceStartRequired ? "campus-entrance" : "nearest-node"
1774      }, this.activeRouteCorrelationId);
1775      if (this.shouldSuspendActiveExternalRoute()) {
1776        this.enterRouteSuspension();
1777        throw new RouteCalculationSuspendedError();
1778      }
1779      if (!path || path.length === 0) {
1780        console.error("[NavigationService] \u274C No path found to destination");
1781        throw new Error("No path found to destination");
1782      }
1783      if (routeStartFloorId !== ((_s = this.currentUserFloor) == null ? void 0 : _s.entity.id) || (coordinateContext.usesDestinationCampusProjection ? routeStartLocationSequence !== this.locationUpdateSequence : routeStartCampusId !== ((_u = (_t = this.currentUserCampus) == null ? void 0 : _t.entity) == null ? void 0 : _u.id))) {
1784        if (this.shouldSuspendActiveExternalRoute()) {
1785          this.enterRouteSuspension();
1786          throw new RouteCalculationSuspendedError();
1787        }
1788        if (contextRetryCount < 2) {
1789          return this.calculateAndSetRoute(destinationNode, preferStairs, preferElevators, contextRetryCount + 1, requireCampusEntranceStart, isInitialRoute);
1790        }
1791        throw new Error("Location context changed while calculating route");
1792      }
1793      this.configureArrivalOverlayContext(route, this.currentState.target);
1794      const routeCampusId = (_v = coordinateContext.destinationCampus) == null ? void 0 : _v.entity.id;
1795      this.setActiveExternalRouteCoordinateContext(coordinateContext.usesDestinationCampusProjection && routeCampusId && coordinateContext.transformQuality ? {
1796        routeCampusId,
1797        transformQuality: coordinateContext.transformQuality
1798      } : void 0);
1799      const progressInfo = this.calculateRouteProgressForSegment(routeStartPosition, route, 0);
1800      this.lastProgressLocationSequence = this.locationUpdateSequence;
1801      this.lastProgressFloorId = (_w = this.currentUserFloor) == null ? void 0 : _w.entity.id;
1802      const distanceToDestination = Math.max(0, route.totalDistance - progressInfo.distanceFromRouteStart);
1803      const instructionUpdate = this.resolveProgressInstructions(route, progressInfo.distanceFromRouteStart, distanceToDestination);
1804      const directDistanceToDestination = this.calculateDirectDistanceToDestination(routeStartPosition, (_x 
1804= route.segments[route.segments.length - 1]) == null ? void 0 : _x.endNode, route);
1805      const deferInitialGpsArrivalGuidance = this.currentLocationSource === LocationSource.GPS && directDistanceToDestination <= this.getArrivalRadiusCm();
1806      const estimatedTimeRemaining = distanceToDestination / 100 / this.WALKING_SPEED_MS;
1807      const isApproachingCampus = coordinateContext.usesDestinationCampusProjection;
1808      this.updateState({
1809        route,
1810        isApproachingCampus,
1811        currentDirection: isApproachingCampus || deferInitialGpsArrivalGuidance ? null : instructionUpdate.currentDirection,
1812        nextDirections: isApproachingCampus || deferInitialGpsArrivalGuidance ? [] : instructionUpdate.nextDirections,
1813        currentSegmentIndex: isApproachingCampus ? 0 : progressInfo.currentSegmentIndex,
1814        currentPosition: routeStartPosition,
1815        distanceToNextTurn: isApproachingCampus ? 0 : deferInitialGpsArrivalGuidance ? distanceToDestination : instructionUpdate.distanceToNextTurn,
1816        distanceToDestination: isApproachingCampus ? 0 : distanceToDestination,
1817        distanceFromRouteStart: isApproachingCampus ? 0 : progressInfo.distanceFromRouteStart,
1818        estimatedTimeRemaining: isApproachingCampus ? 0 : estimatedTimeRemaining
1819      });
1820      this.diagnosticTimeline.record("route", "calculation-completed", {
1821        segmentCount: route.segments.length,
1822        directionCount: route.directions.length,
1823        distance: Math.round(route.totalDistance),
1824        coordinateSource: coordinateContext.usesDestinationCampusProjection ? "destination-campus-projection" : "physical-location",
1825        routeCampusId: routeCampusId != null ? routeCampusId : null,
1826        geoReferencePointCount: (_z = (_y = coordinateContext.transformQuality) == null ? void 0 : _y.referencePointCount) != null ? _z : null,
1827        geoReferenceReliable: (_B = (_A = coordinateContext.transformQuality) == null ? void 0 : _A.isReliable) != null ? _B : null,
1828        accessLegDistance: Math.round((_D = (_C = route.segments.find((segment) => segment.kind === "access")) == null ? void 0 : _C.distance) != null ? _D : 0)
1829      }, this.activeRouteCorrelationId);
1830    });
1831  }
1832  /**
1833   * Check if a node is an outdoor node
1834   * Outdoor = type 'Outdoor' OR no floor/building assigned
1835   */
1836  isOutdoorNode(node) {
1837    return node.entity.type === "Outdoor" || !node.entity.floorId && !node.entity.buildingId;
1838  }
1839  isCampusEntranceNode(node) {
1840    return node.entity.type === "Outdoor" && node.entity.isCampusEntrance === true;
1841  }
1842  /**
1843   * Ground-floor building and floor destinations complete at the entrance. If
1844   * the configured destination node is an indoor door, GPS users may still be
1845   * outside when they reach it, so the arrival overlay uses level-aware
1846   * matching instead of requiring the exact floor ID.
1847   */
1848  configureArrivalOverlayContext(route, target) {
1849    var _a;
1850    if ((target == null ? void 0 : target.type) !== "building" && (target == null ? void 0 : target.type) !== "floor") {
1851      return;
1852    }
1853    const arrivalOverlay = (_a = route.overlays) == null ? void 0 : _a.find((overlay) => overlay.type === RouteOverlayType.Arrive);
1854    if (arrivalOverlay) {
1855      arrivalOverlay.requiresDestinationFloorMatch = !this.isGroundFloor(arrivalOverlay.destinationFloorId);
1856    }
1857  }
1858  /**
1859   * Check whether the current location context is valid for completing the
1860   * active destination. Ground-floor building and floor destinations allow
1861   * outdoor or level-1 context because the target is the building entrance.
1862   */
1863  isAtDestinationContext(destinationNode, target) {
1864    var _a, _b;
1865    if (!destinationNode) {
1866      return false;
1867    }
1868    const destinationFloorId = destinationNode.entity.floorId;
1869    const currentFloorId = (_b = (_a = this.currentUserFloor) == null ? void 0 : _a.entity) == null ? void 0 : _b.id;
1870    if (destinationFloorId) {
1871      if (currentFloorId === destinationFloorId) {
1872        return true;
1873      }
1874      if (((target == null ? void 0 : target.type) === "building" || (target == null ? void 0 : target.type) === "floor") && this.isGroundFloor(destinationFloorId)) {
1875        return !currentFloorId || this.isGroundFloor(currentFloorId);
1876      }
1877      return false;
1878    }
1879    if ((target == null ? void 0 : target.type) === "building" && destinationNode.entity.type === "Door" && !currentFloorId) {
1880      return true;
1881    }
1882    return this.isOutdoorNode(destinationNode) && !currentFloorId;
1883  }
1884  isGroundFloor(floorId) {
1885    var _a, _b, _c;
1886    if (!floorId) {
1887      return false;
1888    }
1889    const floor = this.databaseService.Models.get(floorId);
1890    if (((_a = floor == null ? void 0 : floor.entity) == null ? void 0 : _a.level) === 1) {
1891      return true;
1892    }
1893    const normalizedName = (_c = (_b = floor == null ? void 0 : floor.entity) == null ? void 0 : _b.name) == null ? void 0 : _c.trim().toLocaleLowerCase().replace(/[\s_-]+/g, "");
1894    return normalizedName === "groundfloor";
1895  }
1896  /**
1897   * Find nearest node from current location (works for both indoor and outdoor)
1898   *
1899   * @param position - Current position in map coordinates
1900   * @param floor - Optional floor model (for indoor positioning)
1901   * @param campus - Optional campus model (for outdoor positioning)
1902   * @returns Nearest eligible node; graph reachability is checked separately
1903   */
1904  findNearestNodeFromLocation(position, floor, campus, options = {}) {
1905    var _a, _b, _c;
1906    const effectiveCampus = campus != null ? campus : options.fallbackCampus;
1907    console.log("[NavigationService] findNearestNodeFromLocation:", {
1908      hasFloor: !!floor,
1909      floorId: (_a = floor == null ? void 0 : floor.entity) == null ? void 0 : _a.id,
1910      hasCampus: !!effectiveCampus,
1911      campusId: (_b = effectiveCampus == null ? void 0 : effectiveCampus.entity) == null ? void 0 : _b.id,
1912      usingFallbackCampus: !campus && !!options.fallbackCampus,
1913      position
1914    });
1915    let nodesToSearch = [];
1916    if (options.requireCampusEntranceStart) {
1917      const allNodes = this.getActiveNodes();
1918      nodesToSearch = effectiveCampus ? allNodes.filter((node) => {
1919        var _a2;
1920        return this.getNodeCampusId(node) === ((_a2 = effectiveCampus.entity) == null ? void 0 : _a2.id) && this.isCampusEntranceNode(node);
1921      }) : [];
1922      console.log(`[NavigationService] Searching ${nodesToSearch.length} configured campus entrances`);
1923    } else if (floor) {
1924      nodesToSearch = this.getIndoorNodesForFloorContext(floor);
1925      console.log(`[NavigationService] Searching ${nodesToSearch.length} indoor nodes on floor ${floor.entity.id}`);
1926    } else if (effectiveCampus) {
1927      console.log(`[NavigationService] Searching campus nodes on campus ${(_c = effectiveCampus.entity) == null ? void 0 : _c.id}`);
1928      const allNodes = this.getActiveNodes();
1929      const campusNodes = allNodes.filter((node) => {
1930        var _a2;
1931        return this.getNodeCampusId(node) === ((_a2 = effectiveCampus.entity) == null ? void 0 : _a2.id);
1932      });
1933      const outdoorNodes = campusNodes.filter((node) => this.isOutdoorNode(node));
1934      nodesToSearch = options.preferOutdoorNodes && outdoorNodes.length > 0 ? outdoorNodes : campusNodes;
1935      console.log(`[NavigationService] Found ${nodesToSearch.length} candidate nodes on campus`, {
1936        campusNodeCount: campusNodes.length,
1937        outdoorNodeCount: outdoorNodes.length,
1938        preferOutdoorNodes: !!options.preferOutdoorNodes
1939      });
1940    } else {
1941      console.log("[NavigationService] No floor or campus context - searching all nodes as fallback");
1942      const allNodes = this.getActiveNodes();
1943      const MAX_FALLBACK_DISTANCE = 5e3;
1944      nodesToSearch = allNodes.filter((node) => {
1945        const nodePos = {
1946          x: node.entity.locationX || 0,
1947          y: node.entity.locationY || 0
1948        };
1949        const distance = MathHelpers.calculateDistance(position, nodePos);
1950        return distance <= MAX_FALLBACK_DISTANCE;
1951      });
1952      console.log(`[NavigationService] Found ${nodesToSearch.length} nodes within 50m fallback radius`);
1953    }
1954    if (nodesToSearch.length === 0) {
1955      console.error("[NavigationService] No nodes found to search");
1956      return null;
1957    }
1958    let nearestNode = null;
1959    let minDistance = Infinity;
1960    for (const node of nodesToSearch) {
1961      const nodePos = {
1962        x: node.entity.locationX || 0,
1963        y: node.entity.locationY || 0
1964      };
1965      const distance = MathHelpers.calculateDistance(position, nodePos);
1966      if (distance < minDistance || distance === minDistance && !!nearestNode && node.entity.id < nearestNode.entity.id) {
1967        minDistance = distance;
1968        nearestNode = node;
1969      }
1970    }
1971    if (nearestNode) {
1972      console.log("[NavigationService] Found nearest node:", {
1973        nodeId: nearestNode.entity.id,
1974        nodeType: nearestNode.entity.type,
1975        distance: Math.round(minDistance),
1976        distanceMeters: (minDistance / 100).toFixed(1) + "m"
1977      });
1978      const MAX_OUTDOOR_START_DISTANCE = 15e3;
1979      const MAX_INDOOR_START_DISTANCE = 3e3;
1980      const maxDistance = floor ? MAX_INDOOR_START_DISTANCE : MAX_OUTDOOR_START_DISTANCE;
1981      if (minDistance > maxDistance && !options.allowOutsideCampusRouteStart && !options.requireCampusEntranceStart) {
1982        console.log(`[NavigationService] \u26A0\uFE0F Nearest node too far: ${(minDistance / 100).toFixed(0)}m > ${maxDistance / 100}m threshold`);
1983        return null;
1984      }
1985      if (minDistance > maxDistance) {
1986        console.log("[NavigationService] Using destination campus outdoor graph for an outside-campu
1986s GPS start");
1987      }
1988    } else {
1989      console.error("[NavigationService] No nearest node found");
1990    }
1991    return nearestNode;
1992  }
1993  getIndoorNodesForFloorContext(floor) {
1994    var _a, _b;
1995    const floorId = floor.entity.id;
1996    const relationNodes = (_b = (_a = floor.nodes) == null ? void 0 : _a.filter((node) => !node.entity.isDeleted && !!node.entity.id && (!floorId || node.entity.floorId === floorId))) != null ? _b : [];
1997    if (relationNodes.length > 0) {
1998      return relationNodes;
1999    }
2000    const allNodes = this.getActiveNodes();
2001    const floorNodes = floorId ? allNodes.filter((node) => node.entity.floorId === floorId) : [];
2002    return floorNodes;
2003  }
2004  /**
2005   * Keep the nearest start whenever it has a permitted graph path. A farther
2006   * outdoor start is a bounded recovery for disconnected graphs, never a way
2007   * to shorten a route by inventing an access leg across unmapped obstacles.
2008   */
2009  resolveRouteStart(_0, _1, _2, _3, _4, _5) {
2010    return __async(this, arguments, function* (context, options, destinationNode, preferStairs, preferElevators, penalties, scaleCache = /* @__PURE__ */ new Map()) {
2011      var _a, _b, _c;
2012      const nearestNode = this.findNearestNodeFromLocation(context.position, context.floor, context.searchCampus, options);
2013      if (!nearestNode)
2014        return { nearestNode: null, path: [] };
2015      const routePenalties = __spreadValues({}, penalties != null ? penalties : this.getEffectiveRoutePenalties(nearestNode, destinationNode));
2016      let attemptedCount = 1;
2017      let path = yield this.calculatePath(nearestNode, destinationNode, preferStairs, preferElevators, routePenalties, scaleCache);
2018      if (path.length === 0 && !context.floor) {
2019        const candidates = this.findRouteStartCandidatesFromLocation(context.position, context.floor, context.searchCampus, options, nearestNode, context.usesDestinationCampusProjection ? context.geographicPoint : void 0, scaleCache);
2020        for (const candidate of candidates) {
2021          if (candidate.node.entity.id === nearestNode.entity.id)
2022            continue;
2023          attemptedCount++;
2024          path = yield this.calculatePath(candidate.node, destinationNode, preferStairs, preferElevators, routePenalties, scaleCache);
2025          if (path.length > 0)
2026            break;
2027        }
2028      }
2029      this.diagnosticTimeline.record("route", "route-start-resolved", {
2030        nearestNodeId: (_a = nearestNode.entity.id) != null ? _a : "unknown",
2031        selectedNodeId: (_c = (_b = path[0]) == null ? void 0 : _b.entity.id) != null ? _c : null,
2032        attemptedCount,
2033        fallbackReason: attemptedCount > 1 ? "no permitted graph path" : null
2034      }, this.activeRouteCorrelationId);
2035      return { nearestNode, path };
2036    });
2037  }
2038  /** Bounded outdoor fallbacks, ordered by access distance rather than route cost. */
2039  findRouteStartCandidatesFromLocation(position, floor, campus, options, nearestNode, geographicPoint, scaleCache = /* @__PURE__ */ new Map()) {
2040    var _a, _b;
2041    if (floor || !((_a = this.databaseService) == null ? void 0 : _a.Models)) {
2042      return [{ node: nearestNode, accessCost: 0 }];
2043    }
2044    const effectiveCampus = campus != null ? campus : options.fallbackCampus;
2045    const campusId = (_b = effectiveCampus == null ? void 0 : effectiveCampus.entity) == null ? void 0 : _b.id;
2046    if (!campusId) {
2047      return [{ node: nearestNode, accessCost: 0 }];
2048    }
2049    const campusNodes = this.getActiveNodes().filter((node) => this.getNodeCampusId(node) === campusId);
2050    const outdoorNodes = campusNodes.filter((node) => this.isOutdoorNode(node));
2051    const entranceNodes = outdoorNodes.filter((node) => this.isCampusEntranceNode(node));
2052    const nodesToScore = options.requireCampusEntranceStart ? entranceNodes : options.preferOutdoorNodes && outdoorNodes.length > 0 ? outdoorNodes : campusNodes;
2053    const nearestAccessCost = this.getRouteStartAccessCost(position, nearestNode, geographicPoint, scaleCache);
2054    if (!Number.isFinite(nearestAccessCost)) {
2055      return [{ node: nearestNode, accessCost: nearestAccessCost }];
2056    }
2057    const candidates = nodesToScore.filter((node) => node.entity.id !== nearestNode.entity.id).map((node) => ({
2058      node,
2059      accessCost: this.getRouteStartAccessCost(position, node, geographicPoint, scaleCache)
2060    })).filter((candidate) => Number.isFinite(candidate.accessCost) && candidate.accessCost <= nearestAccessCost + OUTDOOR_ROUTE_START_CANDIDATE_RADIUS_CM).sort((a, b) => a.accessCost - b.accessCost || (a.node.entity.id < b.node.entity.id ? -1 : a.node.entity.id > b.node.entity.id ? 1 : 0)).slice(0, MAX_OUTDOOR_ROUTE_START_CANDIDATES - 1);
2061    return [{ node: nearestNode, accessCost: nearestAccessCost }, ...candidates];
2062  }
2063  getRouteStartAccessCost(position, node, geographicPoint, scaleCache = /* @__PURE__ */ new Map()) {
2064    if (geographicPoint) {
2065      if (node.entity.latitude != null && node.entity.longitude != null) {
2066        return this.calculateGeographicDistanceCm(geographicPoint, {
2067          latitude: node.entity.latitude,
2068          longitude: node.entity.longitude
2069        });
2070      }
2071      if (!this.getNodeCampusId(node)) {
2072        return Number.POSITIVE_INFINITY;
2073      }
2074    }
2075    const nodePosition = {
2076      x: node.entity.locationX || 0,
2077      y: node.entity.locationY || 0
2078    };
2079    const localDistance = MathHelpers.calculateDistance(position, nodePosition);
2080    return this.isOutdoorNode(node) ? localDistance * this.getCampusLocalToCmScale(this.getNodeCampusId(node), scaleCache) : localDistance;
2081  }
2082  getRouteStartSearchOptions(destinationNode, requireCampusEntranceStart = false, request) {
2083    var _a;
2084    const floor = request ? request.location.floor : this.currentUserFloor;
2085    const campus = request ? request.location.campus : this.currentUserCampus;
2086    const source = request ? request.location.source : this.currentLocationSource;
2087    const fallbackCampus = !floor && !campus ? this.resolveCampusForNode(destinationNode) : void 0;
2088    return {
2089      fallbackCampus,
2090      preferOutdoorNodes: !floor,
2091      requireCampusEntranceStart,
2092      allowOutsideCampusRouteStart: (request ? (_a = request.options) == null ? void 0 : _a.allowOutsideCampusRouteStart : this.allowOutsideCampusRouteStart) || !!fallbackCampus && !floor && !campus && source === LocationSource.GPS
2093    };
2094  }
2095  shouldAllowOutsideCampusRouteStart() {
2096    return !this.currentUserFloor && !this.currentUserCampus && this.currentLocationSource === LocationSource.GPS;
2097  }
2098  /**
2099   * Resolve the coordinates route calculations are allowed to consume. An
2100   * explicitly allowed outside-campus GPS start is projected into the
2101   * destination campus; the fixed Riga fallback point must never enter an
2102   * active route.
2103   */
2104  resolveRouteCoordinateContext(destinationNode, allowExistingLocalGpsRoute = false, request) {
2105    var _a, _b, _c;
2106    const position = request ? request.location.point : this.currentUserPosition;
2107    const floor = request ? request.location.floor : this.currentUserFloor;
2108    const campus = request ? request.location.campus : this.currentUserCampus;
2109    const source = request ? request.location.source : this.currentLocationSource;
2110    const geographic = request ? request.location.geographicPoint : this.currentLocationGeographicPoint;
2111    const allowOutside = request ? (_a = request.options) == null ? void 0 : _a.allowOutsideCampusRouteStart : this.allowOutsideCampusRouteStart;
2112    const isOverride = request ? request.isPositionOverride : (_b = this.debugControlService) == null ? void 0 : _b.isOverrideActive;
2113    if (!position) {
2114      return null;
2115    }
2116    const destinationCampus = ((_c = this.databaseService) == null ? void 0 : _c.Models) ? this.resolveCampusForNode(destinationNode) : destinationNode.campus;
2117    const physicalCampus = campus;
2118    const isOutsideDestinationCampus = !!physicalCampus && !!destinationCampus && physicalCampus.entity.id !== destinationCampus.entity.id;
2119    const requiresDestinationProjection = source === LocationSource.GPS && !floor && (!physicalCampus || allowOutside && isOutsideDestinationCampus) && !(isOverride === true && !geographic);
2120    if (!requiresDestinationProjection || allowExistingLocalGpsRoute && !geographic) {
2121      return {
2122        position: __spreadValues({}, position),
2123        floor,
2124        searchCampus: physicalCampus,
2125        destinationCampus,
2126        geographicPoint: geographic,
2127        usesDestinationCampusProjection: false
2128      };
2129    }
2130    const geographicPoint = geographic;
2131    if (!geographicPoint || !Number.isFinite(geographicPoint.latitude) || !Number.isFinite(geographicPoint.longitude) || !destinationCampus || destinationCampus.entity.isDeleted) {
2132      return null;
2133    }
2134    const campusId = destinationCampus.entity.id;
2135    const relatedFloors = Array.from(this.databaseService.Models.values()).filter((model) => {
2136      var _a2;
2137      return model.type === EntityType.FLOOR && !model.entity.isDeleted && (model.entity.campusId === campusId || ((_a2 = model.campus) == null ? void 0 : _a2.entity.id) === campusId);
2138    });
2139    const geoReference = createCampusGeoReference(destinationCampus, relatedFloors);
2140    if (!geoReference) {
2141      return null;
2142    }
2143    const projectedPosition = transformGPSToMap(geographicPoint, geoReference.transform);
2144    if (!Number.isFinite(projectedPosition.x) || !Number.isFinite(projectedPosition.y)) {
2145      return null;
2146    }
2147    return {
2148      position: projectedPosition,
2149      searchCampus: destinationCampus,
2150      destinationCampus,
2151      geographicPoint,
2152      transformQuality: geoReference.quality,
2153      usesDestinationCampusProjection: true
2154    };
2155  }
2156  resolveCampusForNode(node) {
2157    const campusId = this.getNodeCampusId(node);
2158    return campusId ? this.databaseService.Models.get(campusId) : void 0;
2159  }
2160  setActiveExternalRouteCoordinateContext(context) {
2161    var _a;
2162    this.activeExternalRouteCoordinateContext = context;
2163    (_a = this.externalRouteProjectionActiveSubject) == null ? void 0 : _a.next(!!context);
2164  }
2165  getNodeCampusId(node) {
2166    var _a, _b, _c, _d, _e;
2167    if (node.entity.campusId) {
2168      return node.entity.campusId;
2169    }
2170    if ((_a = node.campus) == null ? void 0 : _a.entity.id) {
2171      return node.campus.entity.id;
2172    }
2173    if (node.entity.floorId) {
2174      const floor = this.databaseService.Models.get(node.entity.floorId);
2175      const floorCampusId = (_c = floor == null ? void 0 : floor.entity.campusId) != null ? _c : (_b = floor == null ? void 0 : floor.campus) == null ? void 0 : _b.entity.id;
2176      if (floorCampusId) {
2177        return floorCampusId;
2178      }
2179      const floorBuildingId = floor == null ? void 0 : floor.entity.buildingId;
2180      if (floorBuildingId) {
2181        const building = this.databaseService.Models.get(floorBuildingId);
2182        if ((_d = building == null ? void 0 : building.entity) == null ? void 0 : _d.campusId) {
2183          return building.entity.campusId;
2184        }
2185      }
2186    }
2187    if (node.entity.buildingId) {
2188      const building = this.databaseService.Models.get(node.entity.buildingId);
2189      if ((_e = building == null ? void 0 : building.entity) == null ? void 0 : _e.campusId) {
2190        return building.entity.campusId;
2191      }
2192    }
2193    return null;
2194  }
2195  getActiveNodes() {
2196    return Array.from(this.databaseService.Models.values()).filter((model) => model.type === EntityType.NODE && !model.entity.isDeleted && !!model.entity.id);
2197  }
2198  isOutsideActiveRouteCampus(route) {
2199    var _a, _b, _c, _d;
2200    if (this.currentUserFloor) {
2201      return false;
2202    }
2203    const destinationNode = (_a = route.segments[route.segments.length - 1]) == null ? void 0 : _a.endNode;
2204    const routeCampusId = destinationNode ? this.getNodeCampusId(destinationNode) : null;
2205    if (!routeCampusId) {
2206      return false;
2207    }
2208    const currentCampusId = (_c = (_b = this.currentUserCampus) == null ? void 0 : _b.entity) == null ? void 0 : _c.id;
2209    if (this.currentLocationSource === LocationSource.GPS && !!this.currentLocationGeographicPoint && ((_d = this.activeExternalRouteCoordinateContext) == null ? void 0 : _d.routeCampusId) === routeCampusId) {
2210      return false;
2211    }
2212    return this.currentLocationSource === LocationSource.GPS && (!currentCampusId || currentCampusId !== routeCampusId);
2213  }
2214  /**
2215   * Find nearest node to a position on a floor
2216   * @deprecated Use findNearestNodeFromLocation instead for better outdoor support
2217   */
2218  findNearestNode(position, floorId) {
2219    const floor = this.databaseService.Models.get(floorId);
2220    if (!floor || !floor.nodes)
2221      return null;
2222    let nearestNode = null;
2223    let minDistance = Infinity;
2224    for (const node of floor.nodes) {
2225      const nodePos = {
2226        x: node.entity.locationX || 0,
2227        y: node.entity.locationY || 0
2228      };
2229      const distance = MathHelpers.calculateDistance(position, nodePos);
2230      if (distance < minDistance) {
2231        minDistance = distance;
2232        nearestNode = node;
2233      }
2234    }
2235    return nearestNode;
2236  }
2237  /**
2238   * Calculate optimal path using Dijkstra's algorithm
2239   */
2240  calculatePath(_0, _1) {
2241    return __async(this, arguments, function* (startNode, endNode, preferStairs = true, preferElevators = true, penalties, scaleCache = /* @__PURE__ */ new Map()) {
2242      var _a, _b;
2243      const connectionCache = /* @__PURE__ */ new Map();
2244      const routePenalties = penalties != null ? penalties : this.getEffectiveRoutePenalties(startNode, endNode);
2245      const graphNodes = /* @__PURE__ */ new Map();
2246      const unvisited = /* @__PURE__ */ new Set();
2247      const allNodes = this.getActiveNodes();
2248      for (const node of allNodes) {
2249        const graphNode = {
2250          node,
2251          distance: node.entity.id === startNode.entity.id ? 0 : Infinity,
2252          previous: null,
2253          visited: false
2254        };
2255        graphNodes.set(node.entity.id, graphNode);
2256        unvisited.add(node.entity.id);
2257      }
2258      while (unvisited.size > 0) {
2259        let currentId = null;
2260        let minDistance = Infinity;
2261        for (const id of unvisited) {
2262          const node = graphNodes.get(id);
2263          if (node && node.distance < minDistance) {
2264            minDistance = node.distance;
2265            currentId = id;
2266          }
2267        }
2268        if (!currentId || minDistance === Infinity) {
2269          break;
2270        }
2271        const current2 = graphNodes.get(currentId);
2272        unvisited.delete(currentId);
2273        current2.visited = true;
2274        if (currentId === endNode.entity.id) {
2275          break;
2276        }
2277        const connections = yield this.getNodeConnections(current2.node, connectionCache);
2278        for (const connection of connections) {
2279          const neighborId = connection.entity.nodeAId === currentId ? connection.entity.nodeBId : connection.entity.nodeAId;
2280          const neighbor = graphNodes.get(neighborId);
2281          if (!neighbor || neighbor.visited)
2282            continue;
2283          if (connection.isCrossFloor) {
2284            const nodeAType = (_a = connection.nodeA) == null ? void 0 : _a.entity.type;
2285            const nodeBType = (_b = connection.nodeB) == null ? void 0 : _b.entity.type;
2286            const isStairsTransition = nodeAType === "Stairs" || nodeBType === "Stairs";
2287            const isElevatorTransition = nodeAType === "Elevator" || nodeBType === "Elevator";
2288            if (!preferStairs && isStairsTransition) {
2289              continue;
2290            }
2291            if (!preferElevators && isElevatorTransition) {
2292              continue;
2293            }
2294          }
2295          const edgeWeight = this.calculateEdgeWeight(current2.node, neighbor.node, connection, routePenalties, scaleCache);
2296          const altDistance = current2.distance + edgeWeight;
2297          if (altDistance < neighbor.distance) {
2298            neighbor.distance = altDistance;
2299            neighbor.previous = current2;
2300          }
2301        }
2302      }
2303      const path = [];
2304      let current = graphNodes.get(endNode.entity.id);
2305      if (!current || current.distance === Infinity) {
2306        return [];
2307      }
2308      while (current) {
2309        path.unshift(current.node);
2310        current = current.previous || void 0;
2311      }
2312      return path;
2313    });
2314  }
2315  /**
2316   * Calculate edge weight for pathfinding
2317   * Uses the effective penalties resolved once for the route calculation.
2318   */
2319  calculateEdgeWeight(nodeA, nodeB, connection, penalties, scaleCache = /* @__PURE__ */ new Map()) {
2320    const posA = {
2321      x: nodeA.entity.locationX || 0,
2322      y: nodeA.entity.locationY || 0
2323    };
2324    const posB = {
2325      x: nodeB.entity.locationX || 0,
2326      y: nodeB.entity.locationY || 0
2327    };
2328    let weight = this.calculateSegmentPhysicalDistance(nodeA, nodeB, posA, posB, scaleCache);
2329    if (connection.isCrossFloor) {
2330      weight *= penalties.crossFloorPenalty;
2331      if (nodeA.entity.type === "Stairs" || nodeB.entity.type === "Stairs") {
2332        weight *= penalties.stairAdditionalPenalty;
2333      }
2334    }
2335    if (nodeA.entity.type === "Door" || nodeB.entity.type === "Door") {
2336      weight *= penalties.doorPenalty;
2337    }
2338    if (penalties.enableOutdoorPreference && connection.isOutdoor) {
2339      weight *= penalties.outdoorWeightMultiplier;
2340    }
2341    if (penalties.enableTransitionPenalty && connection.isIndoorOutdoorTransition) {
2342      weight *= penalties.transitionWeightMultiplier;
2343    }
2344    return weight;
2345  }
2346  getEffectiveRoutePenalties(startNode, endNode) {
2347    var _a;
2348    const campusId = (_a = this.getNodeCampusId(endNode)) != null ? _a : this.getNodeCampusId(startNode);
2349    return this.getRoutePenaltiesForCampusId(campusId);
2350  }
2351  /**
2352   * Calculate distance in the canonical Campus Map V3 coordinate system.
2353   * locationX/locationY, MapLocation.point, and PathSegment.distance are all cm.
2354   */
2355  calculateLocalDistanceCm(posA, posB) {
2356    return MathHelpers.calculateDistance(posA, posB);
2357  }
2358  calculateSegmentPhysicalDistance(nodeA, nodeB, posA, posB, scaleCache = /* @__PURE__ */ new Map()) {
2359    var _a;
2360    const localDistance = MathHelpers.calculateDistance(posA, posB);
2361    if (!this.isOutdoorNode(nodeA) && !this.isOutdoorNode(nodeB)) {
2362      return localDistance;
2363    }
2364    const gpsDistance = this.calculateGpsDistanceCm(nodeA, nodeB);
2365    if (gpsDistance !== null) {
2366      return gpsDistance;
2367    }
2368    const campusId = (_a = this.getNodeCampusId(nodeA)) != null ? _a : this.getNodeCampusId(nodeB);
2369    return localDistance * this.getCampusLocalToCmScale(campusId, scaleCache);
2370  }
2371  calculateGpsDistanceCm(nodeA, nodeB) {
2372    const latA = nodeA.entity.latitude;
2373    const lonA = nodeA.entity.longitude;
2374    const latB = nodeB.entity.latitude;
2375    const lonB = nodeB.entity.longitude;
2376    if (latA == null || lonA == null || latB == null || lonB == null) {
2377      return null;
2378    }
2379    const toRadians = (value) => value * Math.PI / 180;
2380    const deltaLat = toRadians(latB - latA);
2381    const deltaLon = toRadians(lonB - lonA);
2382    const a = Math.sin(deltaLat / 2) ** 2 + Math.cos(toRadians(latA)) * Math.cos(toRadians(latB)) * Math.sin(deltaLon / 2) ** 2;
2383    return 6371e3 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * 100;
2384  }
2385  /** Distance from the actual fix to the destination, independent of route
2386   * projection. Route remaining distance is useful for guidance, but a point
2387   * beside the final segment must never be treated as arrival. */
2388  calculateDirectDistanceToDestination(position, destinationNode, route) {
2389    if (!destinationNode) {
2390      return Number.POSITIVE_INFINITY;
2391    }
2392    const latitude = destinationNode.entity.latitude;
2393    const longitude = destinationNode.entity.longitude;
2394    if (this.currentLocationGeographicPoint && latitude != null && longitude != null) {
2395      return this.calculateGeographicDistanceCm(this.currentLocationGeographicPoint, { latitude, longitude });
2396    }
2397    const destinationPoint = this.getNodePoint(destinationNode);
2398    const finalSegment = route.segments[route.segments.length - 1];
2399    const scale = finalSegment ? this.getSegmentPhysicalScale(finalSegment) : 1;
2400    return MathHelpers.calculateDistance(position, destinationPoint) * scale;
2401  }
2402  calculateGeographicDistanceCm(pointA, pointB) {
2403    const toRadians = (value) => value * Math.PI / 180;
2404    const deltaLat = toRadians(pointB.latitude - pointA.latitude);
2405    const deltaLon = toRadians(pointB.longitude - pointA.longitude);
2406    const a = Math.sin(deltaLat / 2) ** 2 + Math.cos(toRadians(pointA.latitude)) * Math.cos(toRadians(pointB.latitude)) * Math.sin(deltaLon / 2) ** 2;
2407    return 6371e3 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * 100;
2408  }
2409  /** GPS accuracy is an error radius, so accept it within a conservative
2410   * 3–10 m band. Beacon/debug arrival remains the standard 3 m. */
2411  getArrivalRadiusCm() {
2412    if (this.currentLocationSource !== LocationSource.GPS) {
2413      return this.ARRIVAL_THRESHOLD;
2414    }
2415    const reportedAccuracy = this.currentLocationAccuracyCm || this.ARRIVAL_THRESHOLD;
2416    return Math.max(this.ARRIVAL_THRESHOLD, Math.min(reportedAccuracy, 1e3));
2417  }
2418  /**
2419   * Derive a campus-specific conversion for legacy outdoor map units. Using
2420   * the median of GPS-referenced node pairs avoids a hard-coded Riga-only
2421   * decimeter assumption while keeping canonical route distances in cm.
2422   */
2423  getCampusLocalToCmScale(campusId, scaleCache = /* @__PURE__ */ new Map()) {
2424    if (!campusId) {
2425      return 1;
2426    }
2427    const cached = scaleCache.get(campusId);
2428    if (cached !== void 0) {
2429      return cached;
2430    }
2431    const references = this.getActiveNodes().filter((node) => this.getNodeCampusId(node) === campusId && node.hasGpsCoordinates).slice(0, 80);
2432    const ratios = [];
2433    for (let i = 0; i < references.length; i++) {
2434      let nearest = null;
2435      let nearestLocalDistance = Number.POSITIVE_INFINITY;
2436      const positionA = this.getNodePoint(references[i]);
2437      for (let j = 0; j < references.length; j++) {
2438        if (i === j)
2439          continue;
2440        const localDistance = MathHelpers.calculateDistance(positionA, this.getNodePoint(references[j]));
2441        if (localDistance > 0.01 && localDistance < nearestLocalDistance) {
2442          nearest = references[j];
2443          nearestLocalDistance = localDistance;
2444        }
2445      }
2446      if (!nearest)
2447        continue;
2448      const gpsDistance = this.calculateGpsDistanceCm(references[i], nearest);
2449      const ratio = gpsDistance === null ? NaN : gpsDistance / nearestLocalDistance;
2450      if (Number.isFinite(ratio) && ratio >= 0.1 && ratio <= 1e3) {
2451        ratios.push(ratio);
2452      }
2453    }
2454    ratios.sort((a, b) => a - b);
2455    const scale = ratios.length > 0 ? ratios[Math.floor(ratios.length / 2)] : 1;
2456    scaleCache.set(campusId, scale);
2457    return scale;
2458  }
2459  getNodePoint(node) {
2460    var _a, _b;
2461    return {
2462      x: (_a = node.entity.locationX) != null ? _a : 0,
2463      y: (_b = node.entity.locationY) != null ? _b : 0
2464    };
2465  }
2466  getSegmentPhysicalScale(segment) {
2467    var _a;
2468    if (!segment) {
2469      return 1;
2470    }
2471    const localDistance = (_a = segment.localDistance) != null ? _a : MathHelpers.calculateDistance(this.getNodePoint(segment.startNode), this.getNodePoint(segment.endNode));
2472    return localDistance > 0 ? segment.distance / localDistance : 1;
2473  }
2474  // ===== Debug Penalty Methods =====
2475  /**
2476   * Get the effective penalties for a campus, including any temporary debug override.
2477   */
2478  getRoutePenaltiesForCampusId(campusId) {
2479    return this.debugPenalties ? __spreadValues({}, this.debugPenalties) : this.campusConfigurationService.getPathfindingPenaltiesForCampusId(campusId);
2480  }
2481  /**
2482   * Set debug penalties for route calculation testing
2483   * Pass null to clear and use default values
2484   */
2485  setDebugPenalties(config) {
2486    this.debugPenalties = config ? __spreadValues({}, config) : null;
2487  }
2488  /**
2489   * Get current debug penalties or defaults if not set
2490   */
2491  getDebugPenalties() {
2492    return this.debugPenalties ? __spreadValues({}, this.debugPenalties) : cloneDefaultRoutePenalties();
2493  }
2494  /**
2495   * Check if debug penalties are currently active
2496   */
2497  hasDebugPenalties() {
2498    return this.debugPenalties !== null;
2499  }
2500  /**
2501   * Calculate navigation heading (bearing) from current position toward next waypoint
2502   * @param from Current position
2503   * @param to Target waypoint position
2504   * @param fallbackSegment Heading segment whose own direction is used when the
2505   *   position coincides with the waypoint, instead of the meaningless
2506   *   point-to-point vector
2507   * @returns Bearing in degrees (0-360) where 0 = North, 90 = East, 180 = South, 270 = West
2508   */
2509  calculateNavigationHeading(from, to, fallbackSegment) {
2510    const dx = to.x - from.x;
2511    const dy = to.y - from.y;
2512    if (dx * dx + dy * dy < _NavigationService.DEGENERATE_WAYPOINT_DISTANCE_CM_SQ && fallbackSegment) {
2513      const fdx = (fallbackSegment.endNode.entity.locationX || 0) - (fallbackSegment.startNode.entity.locationX || 0);
2514      const fdy = (fallbackSegment.endNode.entity.locationY || 0) - (fallbackSegment.startNode.entity.locationY || 0);
2515      if (fdx * fdx + fdy * fdy >= _NavigationService.DEGENERATE_WAYPOINT_DISTANCE_CM_SQ) {
2516        return (Math.atan2(fdx, -fdy) * 180 / Math.PI + 360) % 360;
2517      }
2518    }
2519    const bearing = Math.atan2(dx, -dy) * 180 / Math.PI;
2520    return (bearing + 360) % 360;
2521  }
2522  hasPassedSegmentEnd(position, segment) {
2523    const start = {
2524      x: segment.startNode.entity.locationX || 0,
2525      y: segment.startNode.entity.locationY || 0
2526    };
2527    const end = {
2528      x: segment.endNode.entity.locationX || 0,
2529      y: segment.endNode.entity.locationY || 0
2530    };
2531    const segmentX = end.x - start.x;
2532    const segmentY = end.y - start.y;
2533    const lengthSquared = segmentX * segmentX + segmentY * segmentY;
2534    if (lengthSquared === 0) {
2535      return false;
2536    }
2537    const projection = ((position.x - start.x) * segmentX + (position.y - start.y) * segmentY) / lengthSquared;
2538    return projection >= 1 - Number.EPSILON;
2539  }
2540  resolveNavigationHeadingSegment(route, currentSegmentIndex, position) {
2541    const currentSegment = route.segments[currentSegmentIndex];
2542    const nextSegment = route.segments[currentSegmentIndex + 1];
2543    if (!nextSegment || !this.hasPassedSegmentEnd(position, currentSegment)) {
2544      return currentSegment;
2545    }
2546    const nextStart = {
2547      x: nextSegment.startNode.entity.locationX || 0,
2548      y: nextSegment.startNode.entity.locationY || 0
2549    };
2550    const nextEnd = {
2551      x: nextSegment.endNode.entity.locationX || 0,
2552      y: nextSegment.endNode.entity.locationY || 0
2553    };
2554    const physicalDistanceToNextSegment = MathHelpers.pointToLineDistance(position, nextStart, nextEnd) * this.getSegmentPhysicalScale(nextSegment);
2555    return physicalDistanceToNextSegment <= 500 ? nextSegment : currentSegment;
2556  }
2557  /**
2558   * Get connections for a node (with caching)
2559   */
2560  getNodeConnections(_0) {
2561    return __async(this, arguments, function* (node, connectionCache = /* @__PURE__ */ new Map()) {
2562      const cacheKey = node.entity.id;
2563      if (connectionCache.has(cacheKey)) {
2564        return connectionCache.get(cacheKey);
2565      }
2566      const connections = Array.from(this.databaseService.Models.values()).filter((model) => {
2567        if (model.type !== EntityType.NODE_CONNECTION || model.entity.isDeleted) {
2568          return false;
2569        }
2570        const connection = model;
2571        return !!connection.entity.nodeAId && !!connection.entity.nodeBId && (connection.entity.nodeAId === node.entity.id || connection.entity.nodeBId === node.entity.id);
2572      });
2573      connectionCache.set(cacheKey, connections);
2574      return connections;
2575    });
2576  }
2577  /**
2578   * Create navigation route from path
2579   */
2580  createNavigationRoute(path, startPosition, startGeographicPoint, isCampusApproach = false, scaleCache = /* @__PURE__ */ new Map()) {
2581    const segments = [];
2582    if (isCampusApproach && path.length > 0) {
2583      startPosition = this.getNodePoint(path[0]);
2584    }
2585    if (!isCampusApproach && path.length > 0) {
2586      const firstNode = path[0];
2587      const firstPosition = this.getNodePoint(firstNode);
2588      const localDistance = MathHelpers.calculateDistance(startPosition, firstPosition);
2589      if (localDistance > 0.01) {
2590        const startEntity = Object.assign(new Node(), {
2591          id: `route-start:${crypto.randomUUID()}`,
2592          campusId: firstNode.entity.campusId,
2593          buildingId: firstNode.entity.buildingId,
2594          floorId: firstNode.entity.floorId,
2595          type: firstNode.entity.type,
2596          locationX: startPosition.x,
2597          locationY: startPosition.y,
2598          latitude: startGeographicPoint == null ? void 0 : startGeographicPoint.latitude,
2599          longitude: startGeographicPoint == null ? void 0 : startGeographicPoint.longitude
2600        });
2601        const startNode = new NodeModel(startEntity);
2602        startNode.campus = firstNode.campus;
2603        startNode.floor = firstNode.floor;
2604        segments.push({
2605          startNode,
2606          endNode: firstNode,
2607          localDistance,
2608          distance: this.calculateSegmentPhysicalDistance(startNode, firstNode, startPosition, firstPosition, scaleCache),
2609          kind: "access"
2610        });
2611      }
2612    }
2613    for (let i = 0; i < path.length - 1; i++) {
2614      const startNode = path[i];
2615      const endNode = path[i + 1];
2616      const startPos = {
2617        x: startNode.entity.locationX || 0,
2618        y: startNode.entity.locationY || 0
2619      };
2620      const endPos = {
2621        x: endNode.entity.locationX || 0,
2622        y: endNode.entity.locationY || 0
2623      };
2624      const localDistance = MathHelpers.calculateDistance(startPos, endPos);
2625      segments.push({
2626        startNode,
2627        endNode,
2628        localDistance,
2629        distance: this.calculateSegmentPhysicalDistance(startNode, endNode, startPos, endPos, scaleCache),
2630        kind: "network"
2631      });
2632    }
2633    const endPosition = path.length > 0 ? {
2634      x: path[path.length - 1].entity.locationX || 0,
2635      y: path[path.length - 1].entity.locationY || 0
2636    } : startPosition;
2637    const floorLevels = this.createFloorLevelsMap(segments);
2638    const stairwells = this.getStairwellsFromModels();
2639    return new NavigationRoute(startPosition, endPosition, segments, this.translateService, this.WALKING_SPEED_MS, floorLevels, stairwells, {
2640      shouldIncludeBuildingTransition: (context) => this.shouldIncludeBuildingTransition(context)
2641    });
2642  }
2643  shouldIncludeBuildingTransition(context) {
2644    const indoorNode = context.type === DirectionType.EnterBuilding ? context.destinationNode : context.sourceNode;
2645    const campusId = this.getNodeCampusId(indoorNode);
2646    return this.campusConfigurationService.areBuildingTransitionsEnabled(campusId);
2647  }
2648  /**
2649   * Create a floor levels map for navigation route generation
2650   */
2651  createFloorLevelsMap(segments) {
2652    const floorLevels = /* @__PURE__ */ new Map();
2653    const floorIds = /* @__PURE__ */ new Set();
2654    segments.forEach((segment) => {
2655      if (segment.startNode.entity.floorId) {
2656        floorIds.add(segment.startNode.entity.floorId);
2657      }
2658      if (segment.endNode.entity.floorId) {
2659        floorIds.add(segment.endNode.entity.floorId);
2660      }
2661    });
2662    floorIds.forEach((floorId) => {
2663      const floor = this.databaseService.Models.get(floorId);
2664      if (floor && floor.entity.level !== null && floor.entity.level !== void 0) {
2665        floorLevels.set(floorId, floor.entity.level);
2666      }
2667    });
2668    return floorLevels;
2669  }
2670  /**
2671   * Get all stairwell models from the database for geometric containment checks.
2672   * Used to expand floor transition overlay bounds to include full stairwell area.
2673   */
2674  getStairwellsFromModels() {
2675    const stairwells = [];
2676    this.databaseService.Models.forEach((model) => {
2677      if (model instanceof StairwellModel) {
2678        stairwells.push(model);
2679      }
2680    });
2681    return stairwells;
2682  }
2683  /**
2684   * Update navigation progress based on current position
2685   */
2686  updateNavigationProgress() {
2687    var _a, _b, _c, _d, _e, _f, _g;
2688    const state = this.currentState;
2689    if (state.mode !== NavigationMode.Navigating || !state.route || !this.currentUserPosition) {
2690      return;
2691    }
2692    if (!this.refreshPositionStatus()) {
2693      return;
2694    }
2695    const route = state.route;
2696    const destinationNode = (_b = state.destinationNode) != null ? _b : (_a = route.segments[route.segments.length - 1]) == null ? void 0 : _a.endNode;
2697    if (!destinationNode) {
2698      return;
2699    }
2700    const coordinateContext = this.resolveRouteCoordinateContext(destinationNode, !this.activeExternalRouteCoordinateContext && !this.currentLocationGeographicPoint);
2701    if (!coordinateContext) {
2702      this.diagnosticTimeline.record("route", "route-coordinate-unavailable", {
2703        destinationNodeId: (_c = destinationNode.entity.id) != null ? _c : null,
2704        hasGeographicPoint: !!this.currentLocationGeographicPoint
2705      }, this.activeRouteCorrelationId);
2706      return;
2707    }
2708    const routeCampusId = (_d = coordinateContext.destinationCampus) == null ? void 0 : _d.entity.id;
2709    this.setActiveExternalRouteCoordinateContext(coordinateContext.usesDestinationCampusProjection && routeCampusId && coordinateContext.transformQuality ? {
2710      routeCampusId,
2711      transformQuality: coordinateContext.transformQuality
2712    } : void 0);
2713    const rawPosition = coordinateContext.position;
2714    if (coordinateContext.usesDestinationCampusProjection) {
2715      this.updateState({
2716        isApproachingCampus: true,
2717        currentDirection: null,
2718        nextDirections: [],
2719        currentPosition: rawPosition,
2720        distanceToNextTurn: 0,
2721        distanceToDestination: 0,
2722        distanceFromRouteStart: 0,
2723        estimatedTimeRemaining: 0,
2724        isOffRoute: false,
2725        isNearingOffRoute: false
2726      });
2727      if (!state.isApproachingCampus && !state.recalculating) {
2728        void this.recalculateRoute();
2729      }
2730      return;
2731    }
2732    if (state.isApproachingCampus) {
2733      if (!state.recalculating) {
2734        void this.recalculateRoute();
2735      }
2736      return;
2737    }
2738    const rawProgressInfo = this.calculateRouteProgress(rawPosition, route);
2739    const hasFreshLocationFix = this.locationUpdateSequence > this.lastProgressLocationSequence;
2740    const validatedSegmentIndex = this.validateSegmentChange(state.currentSegmentIndex, rawProgressInfo.currentSegmentIndex, rawPosition, route, rawProgressInfo.distanceFromRoute, hasFreshLocationFix);
2741    if (hasFreshLocationFix) {
2742      this.lastProgressLocationSequence = this.locationUpdateSequence;
2743      this.lastProgressFloorId = (_e = this.currentUserFloor) == null ? void 0 : _e.entity.id;
2744    }
2745    const progressInfo = validatedSegmentIndex === rawProgressInfo.currentSegmentIndex ? rawProgressInfo : this.calculateRouteProgressForSegment(rawPosition, route, validatedSegmentIndex);
2746    const displayPosition = this.snapPositionToRoute(rawPosition, progressInfo.distanceFromRoute, progressInfo.closestSegmentStart, progressInfo.closestSegmentEnd);
2747    const routeRemainingDistance = Math.max(0, route.totalDistance - progressInfo.distanceFromRouteStart);
2748    const directDistanceToDestination = this.calculateDirectDistanceToDestination(rawPosition, destinationNode, route);
2749    const isAtDestinationContext = this.isAtDestinationContext(destinationNode, state.target);
2750    const offRouteThreshold = this.getOffRouteThreshold();
2751    const warningThreshold = this.getWarningThreshold();
2752    if (this.isOutsideActiveRouteCampus(route)) {
2753      const offCampusDistance = Math.max(progressInfo.distanceFromRoute, offRouteThreshold + 1);
2754      if (!state.isOffRoute) {
2755        console.log("[NavigationService] User outside active route campus - recalculating from nearest campus node");
2756        this.updateState({
2757          currentPosition: rawPosition,
2758          isOffRoute: true,
2759          isNearingOffRoute: false
2760        });
2761        this.lastOffRouteDistance = offCampusDistance;
2762        this.recalculateRoute();
2763      } else if (this.shouldRecalculateWhileOffRoute(offCampusDistance, rawPosition)) {
2764        this.lastOffRouteDistance = offCampusDistance;
2765        this.recalculateRoute();
2766      }
2767      return;
2768    }
2769    const isWithinArrivalZone = directDistanceToDestination <= this.getArrivalRadiusCm() && isAtDestinationContext;
2770    if (isWithinArrivalZone) {
2771      if (this.isArrivalPositionConfirmed()) {
2772        this.handleArrival(displayPosition);
2773        return;
2774      }
2775      this.updateState({ currentPosition: displayPosition });
2776      this.navigationEvent$.next({
2777        type: "updated",
2778        state: this.currentState
2779      });
2780      return;
2781    }
2782    if (isAtDestinationContext && this.isAtRouteEndForArrival(progressInfo, route, rawPosition) && this.isArrivalPositionConfirmed()) {
2783      this.handleArrival(displayPosition);
2784      return;
2785    }
2786    if (progressInfo.distanceFromRoute > offRouteThreshold && !state.isOffRoute) {
2787      console.log("[NavigationService] OFF-ROUTE detected - Distance:", progressInfo.distanceFromRoute, "cm, threshold:", offRouteThreshold, "source:", this.currentLocationSource);
2788    }
2789    if (progressInfo.distanceFromRoute > offRouteThreshold) {
2790      if (!state.isOffRoute) {
2791        console.log("[NavigationService] USER IS OFF-ROUTE! Distance:", progressInfo.distanceFromRoute, "cm");
2792        this.updateState({ isOffRoute: true, isNearingOffRoute: false });
2793        console.log("[NavigationService] Triggering route recalculation...");
2794        this.lastOffRouteDistance = progressInfo.distanceFromRoute;
2795        this.recalculateRoute();
2796      } else {
2797        const shouldRecalculate = this.shouldRecalculateWhileOffRoute(progressInfo.distanceFromRoute, rawPosition);
2798        if (shouldRecalculate) {
2799          console.log("[NavigationService] \u{1F504} Recalculating while off-route - Distance:", Math.round(progressInfo.distanceFromRoute), "cm");
2800          this.lastOffRouteDistance = progressInfo.distanceFromRoute;
2801          this.recalculateRoute();
2802        }
2803      }
2804    } else if (progressInfo.distanceFromRoute > warningThreshold && this.currentLocationSource === LocationSource.GPS) {
2805      if (!state.isNearingOffRoute) {
2806        console.log("[NavigationService] \u26A0\uFE0F WARNING: Nearing off-route - Distance:", Math.round(progressInfo.distanceFromRoute), "cm, warning threshold:", warningThreshold);
2807      }
2808      this.lastOffRouteDistance = 0;
2809      const instructionUpdate = this.resolveProgressInstructions(route, progressInfo.distanceFromRouteStart, routeRemainingDistance);
2810      const remainingDistance = routeRemainingDistance;
2811      const estimatedTimeRemaining = remainingDistance / 100 / this.WALKING_SPEED_MS;
2812      this.updateState({
2813        currentDirection: instructionUpdate.currentDirection,
2814        nextDirections: instructionUpdate.nextDirections,
2815        currentSegmentIndex: progressInfo.currentSegmentIndex,
2816        currentPosition: displayPosition,
2817        distanceToNextTurn: instructionUpdate.distanceToNextTurn,
2818        distanceToDestination: routeRemainingDistance,
2819        distanceFromRouteStart: progressInfo.distanceFromRouteStart,
2820        estimatedTimeRemaining,
2821        isOffRoute: false,
2822        isNearingOffRoute: true
2823        // Warning state
2824      });
2825      this.navigationEvent$.next({
2826        type: "updated",
2827        state: this.currentState
2828      });
2829    } else {
2830      if (state.isOffRoute) {
2831        console.log("[NavigationService] \u2705 User returned to route");
2832      }
2833      if (state.isNearingOffRoute) {
2834        console.log("[NavigationService] \u2705 User back within safe distance");
2835      }
2836      this.lastOffRouteDistance = 0;
2837      const instructionUpdate = this.resolveProgressInstructions(route, progressInfo.distanceFromRouteStart, routeRemainingDistance);
2838      const remainingDistance = routeRemainingDistance;
2839      const estimatedTimeRemaining = remainingDistance / 100 / this.WALKING_SPEED_MS;
2840      this.updateState({
2841        currentDirection: instructionUpdate.currentDirection,
2842        nextDirections: instructionUpdate.nextDirections,
2843        currentSegmentIndex: progressInfo.currentSegmentIndex,
2844        currentPosition: displayPosition,
2845        // Use snapped position for smooth display
2846        distanceToNextTurn: instructionUpdate.distanceToNextTurn,
2847        distanceToDestination: routeRemainingDistance,
2848        distanceFromRouteStart: progressInfo.distanceFromRouteStart,
2849        estimatedTimeRemaining,
2850        isOffRoute: false,
2851        isNearingOffRoute: false
2852      });
2853      this.navigationEvent$.next({
2854        type: "updated",
2855        state: this.currentState
2856      });
2857    }
2858    const trackingFloorId = (_g = (_f = this.currentUserFloor) == null ? void 0 : _f.entity) == null ? void 0 : _g.id;
2859    if (trackingFloorId) {
2860      this.lastKnownFloorId = trackingFloorId;
2861    }
2862  }
2863  /**
2864   * Calculate progress along route
2865   */
2866  calculateRouteProgress(position, route) {
2867    var _a, _b;
2868    const progressFloorId = (_b = (_a = this.currentUserFloor) == null ? void 0 : _a.entity) == null ? void 0 : _b.id;
2869    let minDistance = Infinity;
2870    let currentSegmentIndex = 0;
2871    let distanceToNextNode = 0;
2872    let closestSegmentStart;
2873    let closestSegmentEnd;
2874    let hasSegmentsOnCurrentFloor = false;
2875    for (let i = 0; i < route.segments.length; i++) {
2876      const segment = route.segments[i];
2877      const segmentFloorId = segment.startNode.entity.floorId;
2878      const userIsOutdoor = !progressFloorId;
2879      const segmentIsOutdoor = segment.startNode.entity.type === "Outdoor" || !segmentFloorId;
2880      if (userIsOutdoor && !segmentIsOutdoor) {
2881        continue;
2882      }
2883      if (!userIsOutdoor && !segmentIsOutdoor && segmentFloorId !== progressFloorId) {
2884        continue;
2885      }
2886      hasSegmentsOnCurrentFloor = true;
2887      const startPos = {
2888        x: segment.startNode.entity.locationX || 0,
2889        y: segment.startNode.entity.locationY || 0
2890      };
2891      const endPos = {
2892        x: segment.endNode.entity.locationX || 0,
2893        y: segment.endNode.entity.locationY || 0
2894      };
2895      const segmentScale = this.getSegmentPhysicalScale(segment);
2896      const distanceToSegment = MathHelpers.pointToLineDistance(position, startPos, endPos) * segmentScale;
2897      if (distanceToSegment < minDistance) {
2898        minDistance = distanceToSegment;
2899        currentSegmentIndex = i;
2900        distanceToNextNode = MathHelpers.calculateDistance(position, endPos) * segmentScale;
2901        closestSegmentStart = startPos;
2902        closestSegmentEnd = endPos;
2903      }
2904    }
2905    if (!hasSegmentsOnCurrentFloor) {
2906      console.log("[NavigationService] \u{1F3E2} User on wrong floor - no route segments on current floor:", progressFloorId);
2907      minDistance = 99999;
2908    }
2909    const geometricDistanceFromRouteStart = this.calculateDistanceFromRouteStart(route, currentSegmentIndex, position, closestSegmentStart, closestSegmentEnd);
2910    const distanceFromRouteStart = Math.max(geometricDistanceFromRouteStart, this.getCompletedFloorTransitionProgress(route, currentSegmentIndex, progressFloorId));
2911    return {
2912      currentSegmentIndex,
2913      distanceFromRoute: minDistance,
2914      distanceToNextNode,
2915      distanceFromRouteStart,
2916      closestSegmentStart,
2917      closestSegmentEnd
2918    };
2919  }
2920  /**
2921   * Once positioning has selected a segment on the destination side of a
2922   * vertical connector, keep progress just beyond that connector. The end of
2923   * an elevator/stairs segment often shares the same X/Y coordinate as its
2924   * start, so geometric projection alone can otherwise leave the action at an
2925   * exact boundary and continue presenting it on the destination floor.
2926   */
2927  getCompletedFloorTransitionProgress(route, currentSegmentIndex, currentFloorId) {
2928    if (!currentFloorId || currentSegmentIndex <= 0) {
2929      return 0;
2930    }
2931    let cumulativeDistance = 0;
2932    let completedTransitionDistance = 0;
2933    for (let index = 0; index <= currentSegmentIndex && index < route.segments.length; index++) {
2934      const segment = route.segments[index];
2935      cumulativeDistance += segment.distance;
2936      if (segment.startNode.entity.floorId && segment.startNode.entity.floorId !== currentFloorId && segment.endNode.entity.floorId === currentFloorId) {
2937        completedTransitionDistance = cumulativeDistance + 1;
2938      }
2939    }
2940    return completedTransitionDistance;
2941  }
2942  calculateRouteProgressForSegment(position, route, segmentIndex) {
2943    const segment = route.segments[segmentIndex];
2944    if (!segment) {
2945      return {
2946        currentSegmentIndex: Math.max(0, Math.min(segmentIndex, route.segments.length - 1)),
2947        distanceFromRoute: Infinity,
2948        distanceToNextNode: 0,
2949        distanceFromRouteStart: 0
2950      };
2951    }
2952    const closestSegmentStart = {
2953      x: segment.startNode.entity.locationX || 0,
2954      y: segment.startNode.entity.locationY || 0
2955    };
2956    const closestSegmentEnd = {
2957      x: segment.endNode.entity.locationX || 0,
2958      y: segment.endNode.entity.locationY || 0
2959    };
2960    return {
2961      currentSegmentIndex: segmentIndex,
2962      distanceFromRoute: MathHelpers.pointToLineDistance(position, closestSegmentStart, closestSegmentEnd) * this.getSegmentPhysicalScale(segment),
2963      distanceToNextNode: MathHelpers.calculateDistance(position, closestSegmentEnd) * this.get
2963SegmentPhysicalScale(segment),
2964      distanceFromRouteStart: this.calculateDistanceFromRouteStart(route, segmentIndex, position, closestSegmentStart, closestSegmentEnd),
2965      closestSegmentStart,
2966      closestSegmentEnd
2967    };
2968  }
2969  /**
2970   * Calculate cumulative distance from route start to current position.
2971   * Used for overlay triggering based on distance along route.
2972   *
2973   * @param route - Navigation route
2974   * @param segmentIndex - Current segment index
2975   * @param position - User's current position
2976   * @param segmentStart - Start point of current segment
2977   * @param segmentEnd - End point of current segment
2978   * @returns Cumulative distance in cm from route start
2979   */
2980  calculateDistanceFromRouteStart(route, segmentIndex, position, segmentStart, segmentEnd) {
2981    let cumulativeDistance = 0;
2982    for (let i = 0; i < segmentIndex && i < route.segments.length; i++) {
2983      cumulativeDistance += route.segments[i].distance;
2984    }
2985    if (segmentStart && segmentEnd) {
2986      const segmentLength = MathHelpers.calculateDistance(segmentStart, segmentEnd);
2987      if (segmentLength > 0) {
2988        const distanceAlongSegment = this.projectPointOntoSegment(position, segmentStart, segmentEnd, segmentLength);
2989        cumulativeDistance += distanceAlongSegment * this.getSegmentPhysicalScale(route.segments[segmentIndex]);
2990      }
2991    }
2992    return cumulativeDistance;
2993  }
2994  /**
2995   * Project a point onto a line segment and return distance from segment start.
2996   * Uses dot product to find projection point, clamped to segment bounds.
2997   *
2998   * @param point - Point to project
2999   * @param segmentStart - Start of line segment
3000   * @param segmentEnd - End of line segment
3001   * @param segmentLength - Pre-calculated length of segment
3002   * @returns Distance along segment from start (0 to segmentLength)
3003   */
3004  projectPointOntoSegment(point, segmentStart, segmentEnd, segmentLength) {
3005    const dx = point.x - segmentStart.x;
3006    const dy = point.y - segmentStart.y;
3007    const segDx = segmentEnd.x - segmentStart.x;
3008    const segDy = segmentEnd.y - segmentStart.y;
3009    const dotProduct = (dx * segDx + dy * segDy) / segmentLength;
3010    return Math.max(0, Math.min(segmentLength, dotProduct));
3011  }
3012  /**
3013   * Validate segment change with hysteresis to prevent noise-induced jumps
3014   * while allowing genuine forward/backward movement.
3015   *
3016   * When enabled, requires position to be significantly closer (threshold cm)
3017   * to a different segment before switching. This prevents:
3018   * - Jumping forward due to BLE noise
3019   * - Oscillating between segments at boundaries
3020   *
3021   * @param currentSegmentIndex - Current segment index from state
3022   * @param proposedSegmentIndex - Proposed segment index from calculateRouteProgress
3023   * @param position - User's current position
3024   * @param route - Active navigation route
3025   * @param proposedDistanceFromRoute - Distance from position to proposed segment
3026   * @returns Validated segment index (may stay at current if change not significant)
3027   */
3028  validateSegmentChange(currentSegmentIndex, proposedSegmentIndex, position, route, proposedDistanceFromRoute, hasFreshLocationFix) {
3029    if (this.isExpectedFloorTransition(currentSegmentIndex, proposedSegmentIndex, route, hasFreshLocationFix)) {
3030      return proposedSegmentIndex;
3031    }
3032    const progressionGuardedSegmentIndex = this.validateSegmentProgression(currentSegmentIndex, proposedSegmentIndex, hasFreshLocationFix);
3033    if (progressionGuardedSegmentIndex !== proposedSegmentIndex) {
3034      return progressionGuardedSegmentIndex;
3035    }
3036    const settings = this.settingsService.currentSettings;
3037    if (proposedSegmentIndex === currentSegmentIndex) {
3038      return currentSegmentIndex;
3039    }
3040    const threshold = settings.enableSegmentHysteresis ? settings.segmentHysteresisThreshold || 100 : 100;
3041    const currentSegment = route.segments[currentSegmentIndex];
3042    if (!currentSegment) {
3043      return proposedSegmentIndex;
3044    }
3045    const currentSegmentStart = {
3046      x: currentSegment.startNode.entity.locationX || 0,
3047      y: currentSegment.startNode.entity.locationY || 0
3048    };
3049    const currentSegmentEnd = {
3050      x: currentSegment.endNode.entity.locationX || 0,
3051      y: currentSegment.endNode.entity.locationY || 0
3052    };
3053    const currentSegmentDistance = MathHelpers.pointToLineDistance(position, currentSegmentStart, currentSegmentEnd) * this.get
3053SegmentPhysicalScale(currentSegment);
3054    if (proposedDistanceFromRoute < currentSegmentDistance - threshold) {
3055      const direction = proposedSegmentIndex > currentSegmentIndex ? "forward" : "backward";
3056      console.log(`[NavigationService] Segment ${direction} (hysteresis):`, currentSegmentIndex, "\u2192", proposedSegmentIndex, "| new dist:", proposedDistanceFromRoute.toFixed(0), "| old dist:", currentSegmentDistance.toFixed(0), "| threshold:", threshold);
3057      return proposedSegmentIndex;
3058    }
3059    return currentSegmentIndex;
3060  }
3061  validateSegmentProgression(currentSegmentIndex, proposedSegmentIndex, hasFreshLocationFix) {
3062    var _a;
3063    if (proposedSegmentIndex === currentSegmentIndex) {
3064      return proposedSegmentIndex;
3065    }
3066    if (((_a = this.debugControlService) == null ? void 0 : _a.isOverrideActive) === true) {
3067      return proposedSegmentIndex;
3068    }
3069    if (!hasFreshLocationFix) {
3070      return currentSegmentIndex;
3071    }
3072    return proposedSegmentIndex;
3073  }
3074  /**
3075   * A vertical transition can cover substantial route distance while the
3076   * measured X/Y position barely moves. Accept that floor-changing fix only
3077   * when the active route explicitly connects the previous and current floors
3078   * in the same direction as the proposed progress change.
3079   */
3080  isExpectedFloorTransition(currentSegmentIndex, proposedSegmentIndex, route, hasFreshLocationFix) {
3081    var _a;
3082    const previousFloorId = this.lastProgressFloorId;
3083    const currentFloorId = (_a = this.currentUserFloor) == null ? void 0 : _a.entity.id;
3084    if (!hasFreshLocationFix || proposedSegmentIndex === currentSegmentIndex || !previousFloorId || !currentFloorId || previousFloorId === currentFloorId) {
3085      return false;
3086    }
3087    const proposedSegment = route.segments[proposedSegmentIndex];
3088    if (!proposedSegment || proposedSegment.startNode.entity.floorId !== currentFloorId && proposedSegment.endNode.entity.floorId !== currentFloorId) {
3089      return false;
3090    }
3091    const movingForward = proposedSegmentIndex > currentSegmentIndex;
3092    const firstIndex = Math.min(currentSegmentIndex, proposedSegmentIndex);
3093    const lastIndex = Math.max(currentSegmentIndex, proposedSegmentIndex);
3094    return route.segments.slice(firstIndex, lastIndex + 1).some((segment) => {
3095      const startFloorId = segment.startNode.entity.floorId;
3096      const endFloorId = segment.endNode.entity.floorId;
3097      return movingForward ? startFloorId === previousFloorId && endFloorId === currentFloorId : startFloorId === currentFloorId && endFloorId === previousFloorId;
3098    });
3099  }
3100  resetProgressContinuity() {
3101    this.lastProgressLocationSequence = this.locationUpdateSequence;
3102    this.lastProgressFloorId = void 0;
3103  }
3104  /**
3105   * Snap position to route path if within threshold.
3106   * This reduces GPS jitter and provides smoother visual navigation.
3107   * Uses dynamic threshold: 15m for GPS (outdoor), 5m for Beacon (indoor)
3108   *
3109   * @param position - Raw GPS position
3110   * @param distanceFromRoute - Perpendicular distance from route
3111   * @param segmentStart - Start point of closest route segment
3112   * @param segmentEnd - End point of closest route segment
3113   * @returns Snapped position if within threshold, otherwise original position
3114   */
3115  snapPositionToRoute(position, distanceFromRoute, segmentStart, segmentEnd) {
3116    const snapThreshold = this.getSnapThreshold();
3117    if (distanceFromRoute > snapThreshold || !segmentStart || !segmentEnd) {
3118      return position;
3119    }
3120    const dx = segmentEnd.x - segmentStart.x;
3121    const dy = segmentEnd.y - segmentStart.y;
3122    const lengthSquared = dx * dx + dy * dy;
3123    if (lengthSquared === 0) {
3124      return segmentStart;
3125    }
3126    const t = Math.max(0, Math.min(1, ((position.x - segmentStart.x) * dx + (position.y - segmentStart.y) * dy) / lengthSquared));
3127    const snappedPosition = {
3128      x: segmentStart.x + t * dx,
3129      y: segmentStart.y + t * dy
3130    };
3131    return snappedPosition;
3132  }
3133  resolveProgressInstructions(route, distanceFromRouteStart, distanceToDestination) {
3134    var _a, _b;
3135    const instructionResult = this.progressInstructionService.resolve({
3136      route,
3137      distanceFromRouteStartCm: distanceFromRouteStart,
3138      distanceToDestinationCm: distanceToDestination,
3139      locationSource: (_a = this.currentLocationSource) != null ? _a : LocationSource.BEACON
3140    });
3141    return {
3142      currentDirection: this.progressInstructionService.toDirection(instru
3142ctionResult.current, (_b = route.segments[route.segments.length - 1]) == null ? void 0 : _b.endNode, instructionResult.remainingStepCount),
3143      nextDirections: this.progressInstructionService.toDirections(instructionResult.upcoming),
3144      distanceToNextTurn: instructionResult.distanceToNextActionCm,
3145      remainingStepCount: instructionResult.remainingStepCount
3146    };
3147  }
3148  /**
3149   * GPS points with a large reported error radius can land on the destination
3150   * while the user is still tens of metres away. GPSLocationService derives its
3151   * confidence directly from horizontal accuracy; require a reliable fix before
3152   * allowing a terminal navigation state. Beacon/debug positions are unaffected.
3153   */
3154  isArrivalPositionReliable() {
3155    return this.currentLocationSource !== LocationSource.GPS || this.currentLocationConfidence >= this.MIN_GPS_ARRIVAL_CONFIDENCE;
3156  }
3157  /**
3158   * A GPS route may be calculated from a stale fix that happens to land inside
3159   * the destination radius. Require one new reliable GPS update after start,
3160   * while accepting indoor beacon positions without waiting for GPS at all.
3161   */
3162  isArrivalPositionConfirmed() {
3163    var _a;
3164    if (this.currentLocationSource !== LocationSource.GPS || ((_a = this.debugControlService) == null ? void 0 : _a.isOverrideActive) === true) {
3165      return true;
3166    }
3167    return this.isArrivalPositionReliable() && this.locationUpdateSequence > this.navigationStartLocationSequence;
3168  }
3169  beginArrivalConfirmation() {
3170    this.navigationStartLocationSequence = this.locationUpdateSequence;
3171    this.routeEndArrivalSamples.length = 0;
3172  }
3173  /**
3174   * Route-completion arrival predicate used by the
3175   * ARRIVAL_ON_ROUTE_COMPLETION_ENABLED toggle: the position must project onto
3176   * the final meters of the route, stay close to the route laterally, and the
3177   * user must have stood still for the stability window. The lateral clamp
3178   * prevents stale or far off-route fixes that merely project onto the route
3179   * end from producing a false arrival; the stability window prevents a user
3180   * merely walking through the route end from arriving mid-stride.
3181   */
3182  isAtRouteEndForArrival(progressInfo, route, position) {
3183    if (!this.ARRIVAL_ON_ROUTE_COMPLETION_ENABLED) {
3184      return false;
3185    }
3186    const remainingCm = Math.max(0, route.totalDistance - progressInfo.distanceFromRouteStart);
3187    const maxOffRouteCm = Math.min(this.ROUTE_COMPLETION_MAX_OFF_ROUTE_CM, this.getOffRouteThreshold());
3188    const geometryOk = remainingCm <= this.ROUTE_COMPLETION_MAX_REMAINING_CM && progressInfo.distanceFromRoute <= maxOffRouteCm;
3189    if (!geometryOk) {
3190      this.routeEndArrivalSamples.length = 0;
3191      return false;
3192    }
3193    this.routeEndArrivalSamples.push({ x: position.x, y: position.y, at: Date.now() });
3194    if (this.routeEndArrivalSamples.length > this.ROUTE_COMPLETION_STABLE_SAMPLES) {
3195      this.routeEndArrivalSamples.shift();
3196    }
3197    if (this.routeEndArrivalSamples.length < this.ROUTE_COMPLETION_STABLE_SAMPLES) {
3198      return false;
3199    }
3200    const xs = this.routeEndArrivalSamples.map((sample) => sample.x);
3201    const ys = this.routeEndArrivalSamples.map((sample) => sample.y);
3202    const spread = Math.hypot(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
3203    return spread <= this.ROUTE_COMPLETION_MAX_STABLE_SPREAD_CM;
3204  }
3205  /**
3206   * Handle arrival at destination
3207   */
3208  handleArrival(currentPosition) {
3209    var _a, _b, _c, _d, _e, _f;
3210    this.clearRouteSuspension();
3211    const route = this.currentState.route;
3212    const totalDistance = (_a = route == null ? void 0 : route.totalDistance) != null ? _a : 0;
3213    const instructionUpdate = route ? this.resolveProgressInstructions(route, totalDistance, 0) : null;
3214    this.updateState({
3215      mode: NavigationMode.Arrived,
3216      currentDirection: (_b = instructionUpdate == null ? void 0 : instructionUpdate.currentDirection) != null ? _b : null,
3217      nextDirections: [],
3218      currentPosition: currentPosition != null ? currentPosition : this.currentState.currentPosition,
3219      distanceToNextTurn: 0,
3220      distanceToDestination: 0,
3221      distanceFromRouteStart: totalDistance,
3222      estimatedTimeRemaining: 0
3223    });
3224    this.diagnosticTimeline.record("route", "arrival", {
3225      targetId: (_d = (_c = this.currentState.target) == null ? void 0 : _c.id) != null ? _d : null,
3226      distance: Math.round(totalDistance),
3227      floorId: (_f = (_e = this.currentUserFloor) == null ? void 0 : _e.entity.id) != null ? _f : null
3228    }, this.activeRouteCorrelationId);
3229    this.navigationEvent$.next({
3230      type: "arrived",
3231      state: this.currentState
3232    });
3233  }
3234  /**
3235   * Determine if we should recalculate route while user is off-route
3236   * Uses dynamic interval: 30s for GPS (outdoor), 20s for Beacon (indoor)
3237   */
3238  shouldRecalculateWhileOffRoute(currentDistance, currentPosition) {
3239    var _a, _b;
3240    const now = Date.now();
3241    const timeSinceLastRecalc = now - this.lastRecalculationTime;
3242    if (timeSinceLastRecalc < this.MAX_RECALCULATION_INTERVAL) {
3243      return false;
3244    }
3245    const periodicRecalcInterval = this.getPeriodicRecalcInterval();
3246    if (timeSinceLastRecalc >= periodicRecalcInterval) {
3247      console.log("[NavigationService] \u{1F550} Periodic recalculation - been off-route for", Math.round(timeSinceLastRecalc / 1e3), "seconds, interval:", periodicRecalcInterval / 1e3, "s");
3248      return true;
3249    }
3250    if (currentDistance > this.lastOffRouteDistance + this.SIGNIFICANT_MOVEMENT_THRESHOLD) {
3251      console.log("[NavigationService] \u{1F4CD} Significant movement - distance increased from", Math.round(this.lastOffRouteDistance), "to", Math.round(currentDistance), "cm");
3252      return true;
3253    }
3254    if (this.lastRecalculationPosition) {
3255      const distanceFromLastRecalc = MathHelpers.calculateDistance(currentPosition, this.lastRecalculationPosition);
3256      if (distanceFromLastRecalc > this.SIGNIFICANT_MOVEMENT_THRESHOLD) {
3257        console.log("[NavigationService] \u{1F6B6} User moved", Math.round(distanceFromLastRecalc), "cm from last recalculation point");
3258        return true;
3259      }
3260    }
3261    const offRouteFloorId = (_b = (_a = this.currentUserFloor) == null ? void 0 : _a.entity) == null ? void 0 : _b.id;
3262    if (offRouteFloorId && this.lastKnownFloorId && offRouteFloorId !== this.lastKnownFloorId) {
3263      console.log("[NavigationService] \u{1F3E2} Floor changed while off-route:", this.lastKnownFloorId, "->", offRouteFloorId);
3264      return true;
3265    }
3266    return false;
3267  }
3268  /**
3269   * Update navigation state
3270   */
3271  updateState(updates) {
3272    const currentState = this.currentState;
3273    const newState = __spreadProps(__spreadValues(__spreadValues({}, currentState), updates), {
3274      // Deep copy target to prevent shared reference mutations
3275      target: updates.target !== void 0 ? updates.target : currentState.target ? __spreadValues({}, currentState.target) : null
3276    });
3277    if ("isOffRoute" in updates && updates.isOffRoute !== currentState.isOffRoute) {
3278      console.log("[NavigationService] \u26A0\uFE0F isOffRoute changed:", currentState.isOffRoute, "->", updates.isOffRoute);
3279    }
3280    if ("recalculating" in updates && updates.recalculating !== currentState.recalculating) {
3281      console.log("[NavigationService] \u{1F504} recalculating changed:", currentState.recalculating, "->", updates.recalculating);
3282    }
3283    this.navigationState$.next(newState);
3284  }
3285  ngOnDestroy() {
3286    this.destroy$.next();
3287    this.destroy$.complete();
3288    this.cancelNavigation();
3289  }
3290};
3291_NavigationService.DEGENERATE_WAYPOINT_DISTANCE_CM_SQ = 4;
3292_NavigationService.\u0275fac = function NavigationService_Factory(__ngFactoryType__) {
3293  return new (__ngFactoryType__ || _NavigationService)(\u0275\u0275inject(LocationService), \u0275\u0275inject(DatabaseService), \u0275\u0275inject(AlertController), \u0275\u0275inject(TranslateService), \u0275\u0275inject(SettingsService), \u0275\u0275inject(NavigationStateManagerService), \u0275\u0275inject(NavigationProgressInstructionService), \u0275\u0275inject(CampusConfigurationService), \u0275\u0275inject(DebugControlService), \u0275\u0275inject(NavigationDiagnosticsService));
3294};
3295_NavigationService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _NavigationService, factory: _NavigationService.\u0275fac, providedIn: "root" });
3296var NavigationService = _NavigationService;
3297(() => {
3298  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NavigationService, [{
3299    type: Injectable,
3300    args: [{
3301      providedIn: "root"
3302    }]
3303  }], () => [{ type: LocationService }, { type: DatabaseService }, { type: AlertController }, { type: TranslateService }, { type: SettingsService }, { type: NavigationStateManagerService }, { type: NavigationProgressInstructionService }, { type: CampusConfigurationService }, { type: DebugControlService }, { type: NavigationDiagnosticsService }], null);
3304})();
3305
3306// src/app/common/screenshot-capture/screenshot-capture-registry.service.ts
3307var SURFACE_ATTRIBUTE = "data-screenshot-surface-id";
3308var _ScreenshotCaptureRegistry = class _ScreenshotCaptureRegistry {
3309  constructor() {
3310    this.surfaces = /* @__PURE__ */ new Map();
3311  }
3312  register(surface) {
3313    const existing = this.surfaces.get(surface.id);
3314    if (existing && existing !== surface) {
3315      existing.element.removeAttribute(SURFACE_ATTRIBUTE);
3316    }
3317    surface.element.setAttribute(SURFACE_ATTRIBUTE, surface.id);
3318    this.surfaces.set(surface.id, surface);
3319    return () => {
3320      if (this.surfaces.get(surface.id) !== surface)
3321        return;
3322      surface.element.removeAttribute(SURFACE_ATTRIBUTE);
3323      this.surfaces.delete(surface.id);
3324    };
3325  }
3326  captureAll(options) {
3327    return __async(this, null, function* () {
3328      const surfaces = Array.from(this.surfaces.values()).filter((surface) => surface.element.isConnected);
3329      const settled = yield Promise.all(surfaces.map((surface) => __async(this, null, function* () {
3330        try {
3331          return {
3332            snapshot: __spreadValues({
3333              id: surface.id,
3334              element: surface.element
3335            }, yield surface.captureFrame(options))
3336          };
3337        } catch (error) {
3338          return {
3339            failure: {
3340              id: surface.id,
3341              error: error instanceof Error ? error.message : String(error)
3342            }
3343          };
3344        }
3345      })));
3346      return {
3347        snapshots: settled.flatMap((result) => result.snapshot ? [result.snapshot] : []),
3348        failures: settled.flatMap((result) => result.failure ? [result.failure] : [])
3349      };
3350    });
3351  }
3352  get attributeName() {
3353    return SURFACE_ATTRIBUTE;
3354  }
3355};
3356_ScreenshotCaptureRegistry.\u0275fac = function ScreenshotCaptureRegistry_Factory(__ngFactoryType__) {
3357  return new (__ngFactoryType__ || _ScreenshotCaptureRegistry)();
3358};
3359_ScreenshotCaptureRegistry.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _ScreenshotCaptureRegistry, factory: _ScreenshotCaptureRegistry.\u0275fac, providedIn: "root" });
3360var ScreenshotCaptureRegistry = _ScreenshotCaptureRegistry;
3361(() => {
3362  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ScreenshotCaptureRegistry, [{
3363    type: Injectable,
3364    args: [{ providedIn: "root" }]
3365  }], null, null);
3366})();
3367
3368// src/app/common/screenshot-capture/screenshot-capture.utils.ts
3369function blobToDataUrl(blob) {
3370  return new Promise((resolve, reject) => {
3371    const reader = new FileReader();
3372    reader.onload = () => resolve(String(reader.result));
3373    reader.onerror = () => {
3374      var _a;
3375      return reject((_a = reader.error) != null ? _a : new Error("Failed to encode screenshot."));
3376    };
3377    reader.readAsDataURL(blob);
3378  });
3379}
3380function dataUrlToBlob(dataUrl) {
3381  const match = new RegExp("^data:([^;,]+)?(?:;base64)?,(.*)$", "s").exec(dataUrl);
3382  if (!match) {
3383    throw new Error("Invalid screenshot data URL.");
3384  }
3385  const mimeType = normalizeScreenshotMimeType(match[1]);
3386  const isBase64 = dataUrl.slice(0, dataUrl.indexOf(",")).includes(";base64");
3387  const binary = isBase64 ? atob(match[2]) : decodeURIComponent(match[2]);
3388  const bytes = new Uint8Array(binary.length);
3389  for (let index = 0; index < binary.length; index++) {
3390    bytes[index] = binary.charCodeAt(index);
3391  }
3392  return new Blob([bytes], { type: mimeType });
3393}
3394function attachmentToBlob(attachment) {
3395  return isCapturedScreenshot(attachment) ? attachment.blob : dataUrlToBlob(attachment.dataUrl);
3396}
3397function persistScreenshot(screenshot) {
3398  return __async(this, null, function* () {
3399    return {
3400      dataUrl: yield blobToDataUrl(screenshot.blob),
3401      mimeType: screenshot.mimeType,
3402      width: screenshot.width,
3403      height: screenshot.height,
3404      byteSize: screenshot.byteSize,
3405      diagnostics: screenshot.diagnostics
3406    };
3407  });
3408}
3409function migrateLegacyScreenshot(base64) {
3410  const normalized = base64.includes(",") ? base64.slice(base64.indexOf(",") + 1) : base64;
3411  const mimeType = sniffBase64MimeType(normalized);
3412  return {
3413    dataUrl: `data:${mimeType};base64,${normalized}`,
3414    mimeType,
3415    byteSize: Math.floor(normalized.length * 0.75)
3416  };
3417}
3418function normalizeScreenshotMimeType(value) {
3419  switch (value == null ? void 0 : value.toLowerCase()) {
3420    case "image/png":
3421      return "image/png";
3422    case "image/webp":
3423      return "image/webp";
3424    default:
3425      return "image/jpeg";
3426  }
3427}
3428function screenshotExtension(mimeType) {
3429  switch (mimeType) {
3430    case "image/png":
3431      return "png";
3432    case "image/webp":
3433      return "webp";
3434    default:
3435      return "jpg";
3436  }
3437}
3438function isCapturedScreenshot(value) {
3439  return "blob" in value;
3440}
3441function sniffBase64MimeType(base64) {
3442  if (base64.startsWith("iVBOR"))
3443    return "image/png";
3444  if (base64.startsWith("UklGR"))
3445    return "image/webp";
3446  return "image/jpeg";
3447}
3448
3449// src/app/common/map-renderer-v2/models/map-rendering-diagnostics.ts
3450var DEFAULT_MAP_RENDERING_DEBUG_OPTIONS = {
3451  generateLabelMipmaps: true,
3452  labelCanvasPixelRatio: 2,
3453  forcePowerOfTwoLabelTextures: false,
3454  labelTextureFilter: "linear",
3455  premultiplyLabelAlpha: false,
3456  // Compatibility workaround: a trailing unused vertex prevents missing labels
3457  // on affected Xclipse/Chromium devices.
3458  padLabelVertexBuffer: true
3459};
3460
3461// src/app/services/map-rendering-diagnostics/map-rendering-diagnostics.service.ts
3462var MAX_HISTORY = 20;
3463var MAX_SAMPLES = 3;
3464var _MapRenderingDiagnosticsService = class _MapRenderingDiagnosticsService {
3465  constructor() {
3466    this.controller = null;
3467    this.options = __spreadValues({}, DEFAULT_MAP_RENDERING_DEBUG_OPTIONS);
3468    this.history = [];
3469    this.options$ = new BehaviorSubject(__spreadValues({}, this.options));
3470    this.snapshot$ = new BehaviorSubject(null);
3471  }
3472  register(controller) {
3473    this.controller = controller;
3474    controller.applyDebugOptions(this.options);
3475    this.refresh(false);
3476  }
3477  unregister(controller) {
3478    if (this.controller === controller) {
3479      this.controller = null;
3480      this.snapshot$.next(null);
3481    }
3482  }
3483  refreshFor(controller) {
3484    return this.controller === controller ? this.refresh(false) : this.snapshot$.value;
3485  }
3486  refresh(recordAction = true) {
3487    if (!this.controller)
3488      return null;
3489    const startedAt = nowMs();
3490    try {
3491      const captured = this.controller.captureDiagnostics();
3492      if (recordAction) {
3493        this.record("refresh", true, "Snapshot refreshed", void 0, void 0, {
3494          outcomeCode: "REFRESHED",
3495          durationMs: elapsedMs(startedAt)
3496        });
3497      }
3498      const snapshot = __spreadProps(__spreadValues({}, captured), {
3499        options: __spreadValues({}, this.options),
3500        actionHistory: [...this.history]
3501      });
3502      this.snapshot$.next(snapshot);
3503      return snapshot;
3504    } catch (error) {
3505      if (recordAction) {
3506        this.record("refresh", false, messageOf(error), void 0, void 0, {
3507          outcomeCode: "CAPTURE_FAILED",
3508          durationMs: elapsedMs(startedAt)
3509        });
3510      }
3511      return this.snapshot$.value;
3512    }
3513  }
3514  setGenerateMipmaps(enabled) {
3515    this.setOptions("set-mipmaps", { generateLabelMipmaps: enabled });
3516  }
3517  setCanvasPixelRatio(ratio) {
3518    this.setOptions("set-canvas-scale", { labelCanvasPixelRatio: ratio });
3519  }
3520  setForcePowerOfTwo(enabled) {
3521    this.setOptions("set-power-of-two", { forcePowerOfTwoLabelTextures: enabled });
3522  }
3523  setTextureFilter(filter2) {
3524    this.setOptions("set-texture-filter", { labelTextureFilter: filter2 });
3525  }
3526  setPremultiplyAlpha(enabled) {
3527    this.setOptions("set-premultiply-alpha", { premultiplyLabelAlpha: enabled });
3528  }
3529  setVertexBufferPadding(enabled) {
3530    this.setOptions("set-vertex-buffer-padding", { padLabelVertexBuffer: enabled });
3531  }
3532  reuploadLabels() {
3533    return this.runCountAction("reupload-labels", () => {
3534      var _a, _b;
3535      return (_b = (_a = this.controller) == null ? void 0 : _a.reuploadLabels()) != null ? _b : 0;
3536    });
3537  }
3538  rebuildLabels() {
3539    return this.runCountAction("rebuild-labels", () => {
3540      var _a, _b;
3541      return (_b = (_a = this.controller) == null ? void 0 : _a.rebuildLabels()) != null ? _b : 0;
3542    });
3543  }
3544  captureLabelSamples() {
3545    var _a, _b;
3546    return (_b = (_a = this.controller) == null ? void 0 : _a.captureLabelSamples(MAX_SAMPLES)) != null ? _b : [];
3547  }
3548  testContextLoss() {
3549    return __async(this, null, function* () {
3550      var _a;
3551      const startedAt = nowMs();
3552      if (!this.controller) {
3553        this.record("context-loss-test", false, "No active renderer", void 0, void 0, {
3554          outcomeCode: "RENDERER_NOT_READY",
3555          durationMs: elapsedMs(startedAt)
3556        });
3557        return;
3558      }
3559      const baseline = (_a = this.refresh(false)) == null ? void 0 : _a.capturedAt;
3560      try {
3561        const result = yield this.controller.testContextLoss();
3562        this.record("context-loss-test", result.supported && result.restored, result.message, baseline, { restored: result.restored, timedOut: result.timedOut }, {
3563          outcomeCode: !result.supported ? "CONTEXT_TEST_UNSUPPORTED" : result.timedOut ? "CONTEXT_RESTORE_TIMED_OUT" : result.restored ? "CONTEXT_RESTORED" : "CONTEXT_RESTORE_FAILED",
3564          durationMs: elapsedMs(startedAt)
3565        });
3566      } catch (error) {
3567        this.record("context-loss-test", false, messageOf(error), baseline, void 0, {
3568          outcomeCode: "CONTEXT_TEST_FAILED",
3569          durationMs: elapsedMs(startedAt)
3570        });
3571      }
3572      this.refresh(false);
3573    });
3574  }
3575  recordRendererRestart() {
3576    this.record("restart-renderer", true, "Renderer remount requested", void 0, void 0, {
3577      outcomeCode: "RENDERER_REMOUNT_REQUESTED",
3578      durationMs: 0
3579    });
3580  }
3581  resetOptions() {
3582    var _a, _b;
3583    const startedAt = nowMs();
3584    const previous = __spreadValues({}, this.options);
3585    this.options = __spreadValues({}, DEFAULT_MAP_RENDERING_DEBUG_OPTIONS);
3586    const rebuilt = (_b = (_a = this.controller) == null ? void 0 : _a.applyDebugOptions(this.options)) != null ? _b : 0;
3587    this.options$.next(__spreadValues({}, this.options));
3588    this.record("reset-options", true, `${rebuilt} labels updated`, previous, this.options, {
3589      outcomeCode: "OPTIONS_RESET",
3590      affectedCount: rebuilt,
3591      durationMs: elapsedMs(startedAt)
3592    });
3593    this.refresh(false);
3594  }
3595  getCompactSnapshot() {
3596    var _a;
3597    return (_a = this.refresh(false)) != null ? _a : void 0;
3598  }
3599  getLatestAction(action) {
3600    const latest = this.history[0];
3601    return !action || (latest == null ? void 0 : latest.action) === action ? latest : void 0;
3602  }
3603  createExportPayload() {
3604    return __async(this, arguments, function* (options = {}) {
3605      var _a, _b;
3606      const snapshot = this.refresh(false);
3607      if (!snapshot)
3608        return null;
3609      const labelSamples = yield Promise.all(this.captureLabelSamples().map((sample) => serializeLabelSample(sample, options.includeImages === true)));
3610      return __spreadProps(__spreadValues({}, snapshot), {
3611        exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
3612        reproduction: {
3613          appVersion: APP_VERSION,
3614          environment: environment.production ? "production" : "development",
3615          buildCommit: readBuildCommit(),
3616          platform: Capacitor.getPlatform(),
3617          language: (_b = (_a = globalThis.navigator) == null ? void 0 : _a.language) != null ? _b : "unknown",
3618          threeRevision: snapshot.scene.threeRevision
3619        },
3620        findings: buildFindings(snapshot),
3621        labelSamples
3622      });
3623    });
3624  }
3625  downloadJson() {
3626    return __async(this, null, function* () {
3627      const payload = yield this.createExportPayload();
3628      if (!payload)
3629        return false;
3630      const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
3631      const url = URL.createObjectURL(blob);
3632      try {
3633        const link = document.createElement("a");
3634        link.href = url;
3635        link.download = `stride-rendering-debug-${fileTimestamp()}.json`;
3636        link.click();
3637        return true;
3638      } finally {
3639        URL.revokeObjectURL(url);
3640      }
3641    });
3642  }
3643  setOptions(action, change) {
3644    var _a, _b;
3645    const startedAt = nowMs();
3646    const previous = __spreadValues({}, this.options);
3647    this.options = __spreadValues(__spreadValues({}, this.options), change);
3648    try {
3649      const rebuilt = (_b = (_a = this.controller) == null ? void 0 : _a.applyDebugOptions(this.options)) != null ? _b : 0;
3650      this.options$.next(__spreadValues({}, this.options));
3651      this.record(action, true, `${rebuilt} labels updated`, previous, this.options, {
3652        outcomeCode: "OPTIONS_APPLIED",
3653        affectedCount: rebuilt,
3654        durationMs: elapsedMs(startedAt)
3655      });
3656    } catch (error) {
3657      this.options = previous;
3658      this.options$.next(__spreadValues({}, this.options));
3659      this.record(action, false, messageOf(error), previous, change, {
3660        outcomeCode: "OPTIONS_APPLY_FAILED",
3661        durationMs: elapsedMs(startedAt)
3662      });
3663    }
3664    this.refresh(false);
3665  }
3666  runCountAction(action, run) {
3667    const startedAt = nowMs();
3668    try {
3669      const count = run();
3670      this.record(action, true, `${count} label textures affected`, void 0, void 0, {
3671        outcomeCode: count > 0 ? "LABEL_TEXTURES_AFFECTED" : "NO_LABEL_TEXTURES",
3672        affectedCount: count,
3673        durationMs: elapsedMs(startedAt)
3674      });
3675      this.refresh(false);
3676      return count;
3677    } catch (error) {
3678      this.record(action, false, messageOf(error), void 0, void 0, {
3679        outcomeCode: "LABEL_TEXTURE_ACTION_FAILED",
3680        durationMs: elapsedMs(startedAt)
3681      });
3682      this.refresh(false);
3683      return 0;
3684    }
3685  }
3686  record(action, success, result, previous, next, metadata) {
3687    this.history = [__spreadValues({
3688      timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3689      action,
3690      success,
3691      result,
3692      previous,
3693      next
3694    }, metadata), ...this.history].slice(0, MAX_HISTORY);
3695  }
3696};
3697_MapRenderingDiagnosticsService.\u0275fac = function MapRenderingDiagnosticsService_Factory(__ngFactoryType__) {
3698  return new (__ngFactoryType__ || _MapRenderingDiagnosticsService)();
3699};
3700_MapRenderingDiagnosticsService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _MapRenderingDiagnosticsService, factory: _MapRenderingDiagnosticsService.\u0275fac, providedIn: "root" });
3701var MapRenderingDiagnosticsService = _MapRenderingDiagnosticsService;
3702(() => {
3703  (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(MapRenderingDiagnosticsService, [{
3704    type: Injectable,
3705    args: [{ providedIn: "root" }]
3706  }], null, null);
3707})();
3708function messageOf(error) {
3709  return error instanceof Error ? error.message : String(error);
3710}
3711function nowMs() {
3712  var _a, _b;
3713  return (_b = (_a = globalThis.performance) == null ? void 0 : _a.now()) != null ? _b : Date.now();
3714}
3715function elapsedMs(startedAt) {
3716  return Math.round(Math.max(0, nowMs() - startedAt) * 100) / 100;
3717}
3718function readBuildCommit() {
3719  var _a, _b;
3720  const injected = globalThis.__STRIDE_BUILD_COMMIT__;
3721  if (typeof injected === "string" && injected.trim())
3722    return injected.trim();
3723  const metaCommit = (_b = (_a = globalThis.document) == null ? void 0 : _a.querySelector('meta[name="stride-build-commit"]')) == null ? void 0 : _b.content.trim();
3724  return metaCommit || null;
3725}
3726function serializeLabelSample(sample, includeImage) {
3727  return __async(this, null, function* () {
3728    var _b, _c;
3729    const _a = sample, { dataUrl } = _a, metadata = __objRest(_a, ["dataUrl"]);
3730    const decoded = dataUrl ? decodeDataUrl(dataUrl) : null;
3731    return __spreadProps(__spreadValues({}, metadata), {
3732      image: __spreadValues({
3733        available: !!decoded,
3734        embedded: !!decoded && includeImage,
3735        mediaType: (_b = decoded == null ? void 0 : decoded.mediaType) != null ? _b : null,
3736        byteLength: (_c = decoded == null ? void 0 : decoded.bytes.byteLength) != null ? _c : null,
3737        sha256: decoded ? yield sha256Hex(decoded.bytes) : null
3738      }, decoded && includeImage && dataUrl ? { dataUrl } : {})
3739    });
3740  });
3741}
3742function decodeDataUrl(dataUrl) {
3743  const separator = dataUrl.indexOf(",");
3744  if (separator < 0)
3745    return null;
3746  const header = dataUrl.slice(0, separator);
3747  if (!header.startsWith("data:image/png"))
3748    return null;
3749  const payload = dataUrl.slice(separator + 1);
3750  try {
3751    if (header.includes(";base64")) {
3752      const binary = globalThis.atob(payload);
3753      return {
3754        mediaType: "image/png",
3755        bytes: Uint8Array.from(binary, (character) => character.charCodeAt(0))
3756      };
3757    }
3758    return {
3759      mediaType: "image/png",
3760      bytes: new TextEncoder().encode(decodeURIComponent(payload))
3761    };
3762  } catch {
3763    return null;
3764  }
3765}
3766function sha256Hex(bytes) {
3767  return __async(this, null, function* () {
3768    var _a;
3769    if (!((_a = globalThis.crypto) == null ? void 0 : _a.subtle))
3770      return null;
3771    try {
3772      const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3773      const digest = yield globalThis.crypto.subtle.digest("SHA-256", buffer);
3774      return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
3775    } catch {
3776      return null;
3777    }
3778  });
3779}
3780function buildFindings(snapshot) {
3781  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
3782  const findings = [];
3783  if (!snapshot.available) {
3784    findings.push({
3785      code: "RENDERER_UNAVAILABLE",
3786      severity: "error",
3787      message: "The WebGL renderer or context is unavailable."
3788    });
3789  }
3790  if (snapshot.latestError) {
3791    findings.push({
3792      code: "RENDERER_ERROR",
3793      severity: "error",
3794      message: snapshot.latestError
3795    });
3796  }
3797  if (snapshot.context.lossCount > snapshot.context.restorationCount) {
3798    findings.push({
3799      code: "CONTEXT_RESTORATION_PENDING",
3800      severity: "error",
3801      message: "A WebGL context loss has not been matched by a restoration.",
3802      evidence: {
3803        lossCount: snapshot.context.lossCount,
3804        restorationCount: snapshot.context.restorationCount
3805      }
3806    });
3807  }
3808  if (snapshot.scene.dataState !== "loaded") {
3809    findings.push({
3810      code: "SCENE_DATA_NOT_LOADED",
3811      severity: "info",
3812      message: "Indoor floor data is not currently loaded into the renderer.",
3813      evidence: {
3814        dataState: snapshot.scene.dataState,
3815        inputFloorCount: snapshot.scene.inputFloorCount,
3816        osmRasterTilesActive: snapshot.scene.osmRasterTilesActive
3817      }
3818    });
3819  }
3820  const labelCount = ((_b = (_a = snapshot.labels) == null ? void 0 : _a.roomCount) != null ? _b : 0) + ((_d = (_c = snapshot.labels) == null ? void 0 : _c.buildingCount) != null ? _d : 0);
3821  if (snapshot.scene.dataState === "loaded" && labelCount === 0) {
3822    findings.push({
3823      code: "NO_LABEL_RECORDS",
3824      severity: "warning",
3825      message: "Floor data is loaded, but no room or building label records were created."
3826    });
3827  } else if (labelCount > 0 && ((_f = (_e = snapshot.labels) == null ? void 0 : _e.visibleCount) != null ? _f : 0) === 0) {
3828    findings.push({
3829      code: "NO_VISIBLE_LABELS",
3830      severity: "info",
3831      message: "Label records exist, but none are visible in the current view.",
3832      evidence: { labelCount }
3833    });
3834  }
3835  if (((_h = (_g = snapshot.labels) == null ? void 0 : _g.visibleCount) != null ? _h : 0) > ((_j = (_i = snapshot.labels) == null ? void 0 : _i.texturedCount) != null ? _j : 0)) {
3836    findings.push({
3837      code: "VISIBLE_LABEL_WITHOUT_TEXTURE",
3838      severity: "error",
3839      message: "The visible label count exceeds the number of textured labels.",
3840      evidence: {
3841        visibleCount: (_l = (_k = snapshot.labels) == null ? void 0 : _k.visibleCount) != null ? _l : 0,
3842        texturedCount: (_n = (_m = snapshot.labels) == null ? void 0 : _m.texturedCount) != null ? _n : 0
3843      }
3844    });
3845  }
3846  const expectedWidth = Math.round(snapshot.viewport.width * snapshot.rendererPixelRatio);
3847  const expectedHeight = Math.round(snapshot.viewport.height * snapshot.rendererPixelRatio);
3848  if (snapshot.available && (Math.abs(snapshot.drawingBuffer.width - expectedWidth) > 2 || Math.abs(snapshot.drawingBuffer.height - expectedHeight) > 2)) {
3849    findings.push({
3850      code: "DRAWING_BUFFER_SCALE_MISMATCH",
3851      severity: "warning",
3852      message: "The drawing buffer size does not match the viewport and renderer pixel ratio.",
3853      evidence: {
3854        expectedWidth,
3855        expectedHeight,
3856        actualWidth: snapshot.drawingBuffer.width,
3857        actualHeight: snapshot.drawingBuffer.height,
3858        rendererPixelRatio: snapshot.rendererPixelRatio
3859      }
3860    });
3861  }
3862  return findings;
3863}
3864function fileTimestamp() {
3865  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3866}
3867
3868export {
3869  NavigationDiagnosticsService,
3870  MathHelpers,
3871  createCampusGeoReference,
3872  NavigationProgressInstructionService,
3873  NavigationStartError,
3874  NavigationService,
3875  ScreenshotCaptureRegistry,
3876  blobToDataUrl,
3877  dataUrlToBlob,
3878  attachmentToBlob,
3879  persistScreenshot,
3880  migrateLegacyScreenshot,
3881  normalizeScreenshotMimeType,
3882  screenshotExtension,
3883  DEFAULT_MAP_RENDERING_DEBUG_OPTIONS,
3884  MapRenderingDiagnosticsService
3885};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.