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.