1import { 2 LocationSource 3} from "./chunk-2E4THSOE.js"; 4import { 5 GPSService, 6 TrackedObservable 7} from "./chunk-JISY7VJD.js"; 8import { 9 SettingsService, 10 debugLog 11} from "./chunk-QI7JV4VS.js"; 12import { 13 SyncStateService 14} from "./chunk-OTQ6GTAJ.js"; 15import { 16 DatabaseService 17} from "./chunk-RLJCY5HS.js"; 18import { 19 SyncStage 20} from "./chunk-MAIAEXGG.js"; 21import { 22 BehaviorSubject, 23 Injectable, 24 setClassMetadata, 25 ɵɵdefineInjectable, 26 ɵɵinject 27} from "./chunk-UQ6LLOJ6.js"; 28import { 29 __async, 30 __spreadValues 31} from "./chunk-CO64IJQE.js"; 32
33// src/app/common/point-utils.ts 34var GEOMETRY_EPSILON = 0.01; 35var PointUtils = class _PointUtils { 36 static add(a, b) { 37 return { x: a.x + b.x, y: a.y + b.y }; 38 } 39 static subtract(a, b) { 40 return { x: a.x - b.x, y: a.y - b.y }; 41 } 42 static scale(p, factor) { 43 return { x: p.x * factor, y: p.y * factor }; 44 } 45 static distance(a, b) { 46 const dx = b.x - a.x; 47 const dy = b.y - a.y; 48 return Math.sqrt(dx * dx + dy * dy); 49 } 50 static magnitude(p) { 51 return Math.sqrt(p.x * p.x + p.y * p.y); 52 } 53 static normalize(p) { 54 const mag = _PointUtils.magnitude(p); 55 return mag === 0 ? { x: 0, y: 0 } : _PointUtils.scale(p, 1 / mag); 56 } 57 static dot(a, b) { 58 return a.x * b.x + a.y * b.y; 59 } 60 static lerp(a, b, t) { 61 return { 62 x: a.x + (b.x - a.x) * t, 63 y: a.y + (b.y - a.y) * t 64 }; 65 } 66 static rotate(p, angleRadians, origin = { x: 0, y: 0 }) { 67 const cos = Math.cos(angleRadians); 68 const sin = Math.sin(angleRadians); 69 const dx = p.x - origin.x; 70 const dy = p.y - origin.y; 71 return { 72 x: origin.x + dx * cos - dy * sin, 73 y: origin.y + dx * sin + dy * cos 74 }; 75 } 76 static equals(a, b, epsilon = GEOMETRY_EPSILON) { 77 return Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon; 78 } 79 /** 80 * 2D cross product (z-component of 3D cross product). 81 * Returns positive if b is counter-clockwise from a, negative if clockwise. 82 */ 83 static cross(a, b) { 84 return a.x * b.y - a.y * b.x; 85 } 86 /** 87 * Finds the intersection point of two line segments (p1-p2 and p3-p4). 88 * Uses parametric line intersection with tolerance for numerical stability. 89 * 90 * @returns The intersection point and parameters t (position on segment 1) and u (position on segment 2), 91 * or null if segments don't intersect. t and u are in range [0,1] if intersection is within segments. 92 */ 93 static lineSegmentIntersection(p1, p2, p3, p4, epsilon = GEOMETRY_EPSILON) { 94 const d1x = p2.x - p1.x; 95 const d1y = p2.y - p1.y; 96 const d2x = p4.x - p3.x; 97 const d2y = p4.y - p3.y; 98 const cross = d1x * d2y - d1y * d2x; 99 if (Math.abs(cross) < epsilon) { 100 return null; 101 } 102 const dx = p3.x - p1.x; 103 const dy = p3.y - p1.y; 104 const t = (dx * d2y - dy * d2x) / cross; 105 const u = (dx * d1y - dy * d1x) / cross; 106 if (t >= -epsilon && t <= 1 + epsilon && u >= -epsilon && u <= 1 + epsilon) { 107 return { 108 point: { 109 x: p1.x + t * d1x, 110 y: p1.y + t * d1y 111 }, 112 t: Math.max(0, Math.min(1, t)), 113 u: Math.max(0, Math.min(1, u)) 114 }; 115 } 116 return null; 117 } 118 /** 119 * Calculates the signed area of a polygon. 120 * Positive area = counter-clockwise winding, negative = clockwise. 121 * Uses the shoelace formula. 122 */ 123 static polygonSignedArea(vertices) { 124 if (vertices.length < 3) 125 return 0; 126 let area = 0; 127 for (let i = 0; i < vertices.length; i++) { 128 const j = (i + 1) % vertices.length; 129 area += vertices[i].x * vertices[j].y; 130 area -= vertices[j].x * vertices[i].y; 131 } 132 return area / 2; 133 } 134 /** 135 * Returns true if polygon vertices are in clockwise order. 136 */ 137 static isClockwise(vertices) { 138 return _PointUtils.polygonSignedArea(vertices) < 0; 139 } 140 /** 141 * Ensures polygon vertices are in clockwise order. 142 * Returns a new array if reversal was needed, otherwise returns the original. 143 */ 144 static ensureClockwise(vertices) { 145 if (_PointUtils.isClockwise(vertices)) { 146 return vertices; 147 } 148 return [...vertices].reverse(); 149 } 150 /** 151 * Calculates the centroid (center of mass) of a polygon. 152 */ 153 static polygonCentroid(vertices) { 154 if (vertices.length === 0) 155 return { x: 0, y: 0 }; 156 if (vertices.length === 1) 157 return __spreadValues({}, vertices[0]); 158 if (vertices.length === 2) { 159 return { 160 x: (vertices[0].x + vertices[1].x) / 2, 161 y: (vertices[0].y + vertices[1].y) / 2 162 }; 163 } 164 let cx = 0; 165 let cy = 0; 166 const area = _PointUtils.polygonSignedArea(vertices); 167 if (Math.abs(area) < 1e-4) { 168 for (const v of vertices) { 169 cx += v.x; 170 cy += v.y; 171 } 172 return { x: cx / vertices.length, y: cy / vertices.length }; 173 } 174 for (let i = 0; i < vertices.length; i++) { 175 const j = (i + 1) % vertices.length; 176 const cross = vertices[i].x * vertices[j].y - vertices[j].x * vertices[i].y; 177 cx += (vertices[i].x + vertices[j].x) * cross; 178 cy += (vertices[i].y + vertices[j].y) * cross; 179 } 180 const factor = 1 / (6 * area); 181 return { x: cx * factor, y: cy * factor }; 182 } 183 /**
184 * Computes the axis-aligned bounding box of a polygon. 185 */ 186 static polygonBounds(vertices) { 187 if (vertices.length === 0) 188 return null; 189 let minX = vertices[0].x; 190 let minY = vertices[0].y; 191 let maxX = vertices[0].x; 192 let maxY = vertices[0].y; 193 for (let i = 1; i < vertices.length; i++) { 194 minX = Math.min(minX, vertices[i].x); 195 minY = Math.min(minY, vertices[i].y); 196 maxX = Math.max(maxX, vertices[i].x); 197 maxY = Math.max(maxY, vertices[i].y); 198 } 199 return { minX, minY, maxX, maxY }; 200 } 201 /** 202 * Tests if a point is inside a polygon using the ray casting algorithm. 203 * Casts a horizontal ray from the point to infinity and counts edge crossings. 204 * Odd number of crossings = inside, even = outside. 205 * 206 * Performance: O(n) where n is the number of vertices 207 * 208 * @param point The point to test 209 * @param vertices The polygon vertices (must form a closed polygon, last vertex connects to first) 210 * @returns true if point is inside or on the polygon boundary, false otherwise 211 */ 212 static pointInPolygon(point, vertices) { 213 if (vertices.length < 3) { 214 return false; 215 } 216 let inside = false;
217 const x = point.x; 218 const y = point.y; 219 for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) { 220 const xi = vertices[i].x; 221 const yi = vertices[i].y; 222 const xj = vertices[j].x; 223 const yj = vertices[j].y; 224 const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi; 225 if (intersect) { 226 inside = !inside; 227 } 228 } 229 return inside; 230 } 231}; 232
233// src/app/common/gps-transform.ts 234function createGPSTransform(referencePoints, enableOutlierFiltering = true, maxIterations = 2) { 235 if (referencePoints.length < 2) { 236 throw new Error("Need at least 2 reference points for GPS transformation"); 237 } 238 if (referencePoints.length === 2) { 239 debugLog("[GPS Transform] Using only 2 reference points - rotation accuracy limited"); 240 return createTransformFrom2Points(referencePoints); 241 } 242 let filteredPoints = referencePoints; 243 let allOutliers = []; 244 if (enableOutlierFiltering && referencePoints.length >= 4) { 245 let iteration = 0; 246 let previousOutlierCount = -1; 247 while (iteration < maxIterations) { 248 const { inliers, outliers } = filterOutliers(filteredPoints); 249 if (outliers.length === 0 || outliers.length === previousOutlierCount) { 250 break; 251 } 252 filteredPoints = inliers; 253 allOutliers.push(...outliers); 254 previousOutlierCount = outliers.length; 255 iteration++; 256 debugLog(`[GPS Transform] Iteration ${iteration}: Filtered ${outliers.length} outlier(s), ${inliers.length} points remaining`); 257 } 258 if (allOutliers.length > 0) { 259 debugLog(`[GPS Transform] Total outliers filtered: ${allOutliers.length}`, allOutliers.map((o) => o.id || "unknown")); 260 } 261 } 262 const transform = createTransformFromMultiplePoints(filteredPoints); 263 const quality = validateTransform(filteredPoints, transform, allOutliers, referencePoints.length); 264 transform.quality = quality; 265 return transform; 266} 267function transformGPSToMap(gps, transform) { 268 return { 269 x: transform.a * gps.longitude + transform.b * gps.latitude + transform.tx, 270 y: transform.c * gps.longitude + transform.d * gps.latitude + transform.ty 271 }; 272} 273function transformMapToGPS(map, transform) { 274 const det = transform.a * transform.d - transform.b * transform.c; 275 if (Math.abs(det) < 1e-10) { 276 throw new Error("GPS transformation matrix is singular (not invertible)"); 277 } 278 const dx = map.x - transform.tx; 279 const dy = map.y - transform.ty; 280 return { 281 longitude: (transform.d * dx - transform.b * dy) / det, 282 latitude: (-transform.c * dx + transform.a * dy) / det 283 }; 284} 285function filterOutliers(referencePoints, outlierThreshold = 2.5, minPoints = 3) { 286 if (referencePoints.length <= minPoints) { 287 return { inliers: referencePoints, outliers: [] }; 288 } 289 const initialTransform = referencePoints.length === 2 ? createTransformFrom2Points(referencePoints) : createTransformFromMultiplePoints(referencePoints); 290 const pointErrors = []; 291 for (const ref of referencePoints) { 292 const predicted = transformGPSToMap(ref.gps, initialTransform); 293 const error = Math.sqrt(Math.pow(predicted.x - ref.map.x, 2) + Math.pow(predicted.y - ref.map.y, 2)); 294 pointErrors.push({ point: ref, error }); 295 } 296 const errors = pointErrors.map((pe) => pe.error); 297 const mean = errors.reduce((sum, e) => sum + e, 0) / errors.length; 298 const variance = errors.reduce((sum, e) => sum + Math.pow(e - mean, 2), 0) / errors.length; 299 const stdDev = Math.sqrt(variance); 300 const threshold = mean + outlierThreshold * stdDev; 301 const inliers = []; 302 const outliers = []; 303 pointErrors.sort((a, b) => a.error - b.error); 304 for (const { point, error } of pointErrors) { 305 if (error > threshold && inliers.length >= minPoints) { 306 outliers.push(point); 307 } else { 308 inliers.push(point); 309 } 310 } 311 return { inliers, outliers }; 312} 313function validateTransform(referencePoints, transform, outliers = [], originalCount) { 314 const errors = []; 315 for (const ref of referencePoints) { 316 const predicted = transformGPSToMap(ref.gps, transform); 317 const error = Math.sqrt(Math.pow(predicted.x - ref.map.x, 2) + Math.pow(predicted.y - ref.map.y, 2)); 318 errors.push(error); 319 } 320 const meanSquaredError = errors.reduce((sum, e) => sum + e * e, 0) / errors.length; 321 const maxError = Math.max(...errors); 322 const worstInlierError = maxError; 323 const scaleX = Math.sqrt(transform.a * transform.a + transform.c * transform.c); 324 const scaleY = Math.sqrt(transform.b * transform.b + transform.d * transform.d); 325 const rotationAngle = Math.atan2(transform.c, transform.a) * 180 / Math.PI; 326 const uniformScale = Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) < 0.1; 327 const isReliable = referencePoints.length >= 3 && maxError < 500 && // Less than 5m error in centimeters 328 meanSquaredError < 1e4 && // Less than 1m RMS error 329 scaleX > 0 && scaleY > 0; 330 const outlierCount = outliers.length; 331 const outlierNodeIds = outliers.map((o) => o.id || "unknown"); 332 const inlierCount = referencePoints.length; 333 const totalCount = originalCount !== void 0 ? originalCount : referencePoints.length; 334 return { 335 referencePointCount: totalCount, 336 meanSquaredError, 337 maxError, 338 rotationAngle, 339 scaleX, 340 scaleY, 341 uniformScale, 342 isReliable, 343 outlierCount, 344 outlierNodeIds, 345 inlierCount, 346 worstInlierError, 347 filteredPoints: outliers 348 }; 349} 350function createTransformFrom2Points(referencePoints) { 351 const [p1, p2] = referencePoints; 352 const gpsVector = { 353 lng: p2.gps.longitude - p1.gps.longitude, 354 lat: p2.gps.latitude - p1.gps.latitude 355 }; 356 const mapVector = { 357 x: p2.map.x - p1.map.x, 358 y: p2.map.y - p1.map.y 359 }; 360 const gpsLength = Math.sqrt(gpsVector.lng * gpsVector.lng + gpsVector.lat * gpsVector.lat); 361 const mapLength = Math.sqrt(mapVector.x * mapVector.x + mapVector.y * mapVector.y); 362 if (gpsLength === 0 || mapLength === 0) { 363 throw new Error("Reference points are identical - cannot create transformation"); 364 } 365 const scale = mapLength / gpsLength; 366 const gpsAngle = Math.atan2(gpsVector.lat, gpsVector.lng); 367 const mapAngle = Math.atan2(mapVector.y, mapVector.x); 368 const rotationAngle = mapAngle - gpsAngle; 369 const cos = Math.cos(rotationAngle); 370 const sin = Math.sin(rotationAngle); 371 const a = scale * cos; 372 const b = scale * -sin; 373 const c = scale * sin; 374 const d = scale * cos; 375 const tx = p1.map.x - (a * p1.gps.longitude + b * p1.gps.latitude); 376 const ty = p1.map.y - (c * p1.gps.longitude + d * p1.gps.latitude); 377 const transform = { 378 a, 379 b, 380 c, 381 d, 382 tx, 383 ty, 384 quality: {} 385 // Will be calculated below 386 }; 387 transform.quality = validateTransform(referencePoints, transform); 388 return transform; 389} 390function createTransformFromMultiplePoints(referencePoints) { 391 const n = referencePoints.length; 392 const A = []; 393 const bX = []; 394 const bY = []; 395 for (const ref of referencePoints) { 396 A.push([ref.gps.longitude, ref.gps.latitude, 1]); 397 bX.push(ref.map.x); 398 bY.push(ref.map.y); 399 } 400 const xParams = solveLeastSquares(A, bX); 401 const yParams = solveLeastSquares(A, bY); 402 const transform = { 403 a: xParams[0], 404 // longitude coefficient for X 405 b: xParams[1], 406 // latitude coefficient for X 407 tx: xParams[2], 408 // X translation 409 c: yParams[0], 410 // longitude coefficient for Y 411 d: yParams[1], 412 // latitude coefficient for Y 413 ty: yParams[2], 414 // Y translation 415 quality: {} 416 }; 417 transform.quality = validateTransform(referencePoints, transform); 418 return transform; 419} 420function solveLeastSquares(A, b) { 421 const rows = A.length; 422 const cols = A[0].length; 423 const AtA = Array(cols).fill(0).map(() => Array(cols).fill(0)); 424 for (let i = 0; i < cols; i++) { 425 for (let j = 0; j < cols; j++) { 426 for (let k = 0; k < rows; k++) { 427 AtA[i][j] += A[k][i] * A[k][j]; 428 } 429 } 430 } 431 const Atb = Array(cols).fill(0); 432 for (let i = 0; i < cols; i++) { 433 for (let k = 0; k < rows; k++) { 434 Atb[i] += A[k][i] * b[k]; 435 } 436 } 437 return solveLinearSystem(AtA, Atb); 438} 439function solveLinearSystem(A, b) { 440 const n = A.length; 441 const augmented = A.map((row, i) => [...row, b[i]]); 442 for (let i = 0; i < n; i++) { 443 let maxRow = i; 444 for (let k = i + 1; k < n; k++) { 445 if (Math.abs(augmented[k][i]) > Math.abs(augmented[maxRow][i])) { 446 maxRow = k; 447 } 448 } 449 [augmented[i], augmented[maxRow]] = [augmented[maxRow], augmented[i]]; 450 if (Math.abs(augmented[i][i]) < 1e-10) { 451 throw new Error("Matrix is singular - cannot solve least squares system"); 452 } 453 for (let k = i + 1; k < n; k++) { 454 const factor = augmented[k][i] / augmented[i][i]; 455 for (let j = i; j <= n; j++) { 456 augmented[k][j] -= factor * augmented[i][j]; 457 } 458 } 459 } 460 const x = Array(n).fill(0); 461 for (let i = n - 1; i >= 0; i--) { 462 x[i] = augmented[i][n]; 463 for (let j = i + 1; j < n; j++) { 464 x[i] -= augmented[i][j] * x[j]; 465 } 466 x[i] /= augmented[i][i]; 467 } 468 return x; 469} 470
471// src/app/common/map-renderer-v2/utils/fallback-geo-reference.ts 472var EARTH_RADIUS_CM = 637813700; 473var DEGREES_TO_RADIANS = Math.PI / 180; 474var RIGA_INITIAL_VIEW_CENTER = { 475 latitude: 56.9514208, 476 longitude: 24.1135696 477}; 478function createFallbackGeoReference(center) { 479 const transform = createFallbackTransform(center); 480 return { 481 transform, 482 quality: transform.quality, 483 referencePoints: [ 484 { 485 id: "fallback-center", 486 gps: center, 487 map: { x: 0, y: 0 } 488 }, 489 { 490 id: "fallback-east", 491 gps: { 492 latitude: center.latitude, 493 longitude: center.longitude + 1e-4 494 }, 495 map: transformFallbackGpsToMap({ 496 latitude: center.latitude, 497 longitude: center.longitude + 1e-4 498 }, center) 499 } 500 ] 501 }; 502} 503function transformFallbackGpsToMap(gps, center) { 504 return transformGPSToMap(gps, createFallbackTransform(center)); 505} 506function createFallbackTransform(center) { 507 const latitudeScale = EARTH_RADIUS_CM * DEGREES_TO_RADIANS; 508 const longitudeScale = latitudeScale * Math.cos(center.latitude * DEGREES_TO_RADIANS); 509 const transform = { 510 a: longitudeScale, 511 b: 0, 512 c: 0, 513 d: -latitudeScale, 514 tx: -longitudeScale * center.longitude, 515 ty: latitudeScale * center.latitude, 516 quality: createFallbackQuality(latitudeScale, longitudeScale) 517 }; 518 return transform; 519} 520function createFallbackQuality(latitudeScale, longitudeScale) { 521 return { 522 referencePointCount: 2, 523 meanSquaredError: 0, 524 maxError: 0, 525 rotationAngle: 0, 526 scaleX: longitudeScale, 527 scaleY: latitudeScale, 528 uniformScale: false, 529 isReliable: false, 530 outlierCount: 0, 531 outlierNodeIds: [], 532 inlierCount: 2, 533 worstInlierError: 0, 534 filteredPoints: [] 535 }; 536} 537
538// src/app/services/gps-location/constants/altitude-floor-detection.constants.ts 539var GROUND_LEVEL_OFFSET = 30; 540var FLOOR_HEIGHT = 3; 541var MAX_ALTITUDE_ACCURACY = 10; 542
543// src/app/services/gps-location/core/building-detection.manager.ts 544var BuildingDetectionManager = class { 545 constructor() { 546 this.logger = console; 547 this.buildingBoundsCache = /* @__PURE__ */ new Map(); 548 this.lastDetectedBuilding = null; 549 this.lastDetectedFloor = null; 550 this.lastCheckPoint = null; 551 this.hasLastDetection = false; 552 this.HYSTERESIS_RADIUS = 500; 553 } 554 // ============================================================================ 555 // Public API - Detection 556 // ============================================================================ 557 /** 558 * Detect building and floor from a map point 559 * 560 * @param point Map point in centimeters (x, y) 561 * @param campus Current campus (needed to get buildings) 562 * @param altitude Optional GPS altitude in meters (for altitude-based floor detection) 563 * @param altitudeAccuracy Optional GPS altitude accuracy in meters 564 * @returns Building and floor, or null if point is not in any building 565 */ 566 detect(point, campus, altitude, altitudeAccuracy) { 567 var _a, _b, _c, _d, _e, _f; 568 if (!campus) { 569 return { building: null, floor: null }; 570 } 571 if (this.lastCheckPoint && this.hasLastDetection) { 572 const distance = PointUtils.distance(point, this.lastCheckPoint); 573 if (distance < this.HYSTERESIS_RADIUS) { 574 return { 575 building: this.lastDetectedBuilding, 576 floor: this.lastDetectedFloor 577 }; 578 } 579 } 580 this.ensureBuildingBoundsCache(campus); 581 const result = this.detectBuildingAndFloor(point, campus, altitude, altitudeAccuracy); 582 this.lastCheckPoint = __spreadValues({}, point); 583 this.lastDetectedBuilding = result.building; 584 this.lastDetectedFloor = result.floor; 585 this.hasLastDetection = true; 586 if (((_a = result.building) == null ? void 0 : _a.entity.id) !== ((_b = this.lastDetectedBuilding) == null ? void 0 : _b.entity.id) || ((_c = result.floor) == null ? void 0 : _c.entity.id) !== ((_d = this.lastDetectedFloor) == null ? void 0 : _d.entity.id)) { 587 this.logger.log("BUILDING-DETECTION", "\u{1F3E2} BUILDING/FLOOR DETECTED", { 588 building: ((_e = result.building) == null ? void 0 : _e.entity.name) || "none", 589 floor: ((_f = result.floor) == null ? void 0 : _f.entity.name) || "none", 590 point: { x: point.x.toFixed(0), y: point.y.toFixed(0) } 591 }); 592 } 593 return result; 594 } 595 /** 596 * Reset detection state 597 * Called when campus changes or location is reset 598 */ 599 reset() { 600 this.logger.log("BUILDING-DETECTION", "Resetting building detection state"); 601 this.buildingBoundsCache.clear(); 602 this.lastDetectedBuilding = null; 603 this.lastDetectedFloor = null; 604 this.lastCheckPoint = null; 605 this.hasLastDetection = false; 606 } 607 /** 608 * Get all floors that contain the given point (for debugging) 609 * Unlike detect(), this returns ALL matching floors, not just the first one 610 * 611 * @param point Map point in centimeters (x, y) 612 * @param campus Current campus 613 * @returns Array of all floors containing the point 614 */ 615 getAllMatchingFloors(point, campus) { 616 if (!campus) { 617 return []; 618 } 619 const matchingFloors = []; 620 const buildings = campus.buildings || []; 621 this.ensureBuildingBoundsCache(campus); 622 for (const building of buildings) { 623 if (building.entity.isDeleted) { 624 continue; 625 } 626 const aabb = this.buildingBoundsCache.get(building.entity.id); 627 if (!aabb || !this.pointInAABB(point, aabb)) { 628 continue; 629 } 630 const floors = building.floors || []; 631 for (const floor of floors) { 632 if (floor.entity.isDeleted) { 633 continue; 634 } 635 if (this.pointInFloor(point, floor)) { 636 matchingFloors.push(floor); 637 } 638 } 639 } 640 return matchingFloors; 641 } 642 /** 643 * Get metadata about the last detection (for debugging) 644 * 645 * @returns Detection metadata including whether result was cached 646 */ 647 getDetectionMetadata() { 648 return { 649 isCached: this.hasLastDetection, 650 lastCheckPoint: this.lastCheckPoint ? __spreadValues({}, this.lastCheckPoint) : null 651 }; 652 } 653 // ============================================================================ 654 // Private Methods - Detection Logic 655 // ============================================================================ 656 /** 657 * Detect building and floor using hybrid AABB + polygon approach 658 * 659 * @param point Map point in centimeters (x, y) 660 * @param campus Current campus 661 * @param altitude Optional GPS altitude in meters 662 * @param altitudeAccuracy Optional GPS altitude accuracy in meters 663 */ 664 detectBuildingAndFloor(point, campus, altitude, altitudeAccuracy) { 665 const buildings = campus.buildings || []; 666 if (buildings.length === 0) { 667 return { building: null, floor: null }; 668 } 669 const candidates = []; 670 for (const building of buildings) { 671 if (building.entity.isDeleted) { 672 continue; 673 } 674 const aabb = this.buildingBoundsCache.get(building.entity.id); 675 if (aabb && this.pointInAABB(point, aabb)) { 676 candidates.push(building); 677 } 678 } 679 if (candidates.length === 0) { 680 return { building: null, floor: null }; 681 } 682 for (const building of candidates) { 683 let floor = null; 684 if (altitude != null && altitudeAccuracy != null && altitudeAccuracy <= MAX_ALTITUDE_ACCURACY) { 685 floor = this.detectFloorByAltitude(altitude, building); 686 if (floor) { 687 const inPolygon = this.pointInFloor(point, floor); 688 if (!inPolygon) { 689 floor = null; 690 } 691 } 692 } 693 if (!floor) { 694 floor = this.detectFloorInBuilding(point, building); 695 } 696 if (floor) { 697 return { building, floor }; 698 } 699 } 700 return { building: null, floor: null }; 701 } 702 /** 703 * Detect which floor in a building contains the point 704 * Uses precise polygon containment test 705 */ 706 detectFloorInBuilding(point, building) { 707 const floors = building.floors || []; 708 for (const floor of floors) { 709 if (floor.entity.isDeleted) { 710 continue; 711 } 712 if (this.pointInFloor(point, floor)) { 713 return floor; 714 } 715 } 716 return null; 717 } 718 /** 719 * Detect floor based on GPS altitude 720 * Uses hardcoded ground level offset and floor height constants 721 * 722 * @param altitude GPS altitude in meters 723 * @param building Building to search for matching floor 724 * @returns Floor that matches the calculated floor level, or null if not found 725 */ 726 detectFloorByAltitude(altitude, building) { 727 const calculatedLevel = Math.floor((altitude - GROUND_LEVEL_OFFSET) / FLOOR_HEIGHT) + 1; 728 const floors = building.floors || []; 729 for (const floor of floors) { 730 if (floor.entity.isDeleted) { 731 continue; 732 } 733 if (floor.entity.level === calculatedLevel) { 734 return floor; 735 } 736 } 737 return null; 738 } 739 /** 740 * Test if point is inside a floor's polygon 741 */ 742 pointInFloor(point, floor) { 743 const corners = floor.corners || []; 744 if (corners.length < 3) { 745 return false;
746 } 747 const sortedCorners = corners.filter((corner) => !corner.entity.isDeleted).sort((a, b) => (a.entity.position || 0) - (b.entity.position || 0)); 748 if (sortedCorners.length < 3) { 749 return false; 750 } 751 const vertices = sortedCorners.map((corner) => ({ 752 x: corner.entity.locationX || 0, 753 y: corner.entity.locationY || 0 754 })); 755 return PointUtils.pointInPolygon(point, vertices); 756 } 757 // ============================================================================ 758 // Private Methods - AABB Caching 759 // ============================================================================ 760 /** 761 * Ensure AABB cache is initialized for all buildings in campus 762 * Only computes if cache is empty (called once per campus) 763 */ 764 ensureBuildingBoundsCache(campus) { 765 var _a; 766 if (this.buildingBoundsCache.size > 0) { 767 return; 768 } 769 this.logger.log("BUILDING-DETECTION", "Initializing building AABB cache", { 770 campusId: campus.entity.id, 771 buildingCount: ((_a = campus.buildings) == null ? void 0 : _a.length) || 0 772 }); 773 const buildings = campus.buildings || []; 774 for (const building of buildings) { 775 if (building.entity.isDeleted) { 776 continue; 777 } 778 const aabb = this.computeBuildingAABB(building); 779 if (aabb) { 780 this.buildingBoundsCache.set(building.entity.id, aabb); 781 } 782 } 783 this.logger.log("BUILDING-DETECTION", "Building AABB cache initialized", { 784 cachedBuildings: this.buildingBoundsCache.size 785 }); 786 } 787 /** 788 * Compute axis-aligned bounding box for a building 789 * AABB is computed from all floor outlines in the building 790 */ 791 computeBuildingAABB(building) { 792 const floors = building.floors || []; 793 if (floors.length === 0) { 794 return null; 795 } 796 let minX = Number.MAX_VALUE; 797 let minY = Number.MAX_VALUE; 798 let maxX = Number.MIN_VALUE; 799 let maxY = Number.MIN_VALUE; 800 let hasValidBounds = false; 801 for (const floor of floors) { 802 if (floor.entity.isDeleted) { 803 continue; 804 } 805 const floorBounds = floor.getOutlineBounds(); 806 if (floorBounds) { 807 hasValidBounds = true; 808 minX = Math.min(minX, floorBounds.x); 809 minY = Math.min(minY, floorBounds.y); 810 maxX = Math.max(maxX, floorBounds.x + floorBounds.width); 811 maxY = Math.max(maxY, floorBounds.y + floorBounds.height); 812 } 813 } 814 if (!hasValidBounds) { 815 return null; 816 } 817 return { minX, minY, maxX, maxY }; 818 } 819 /** 820 * Test if point is inside axis-aligned bounding box 821 * Simple 4-comparison test: O(1) 822 */ 823 pointInAABB(point, aabb) { 824 return point.x >= aabb.minX && point.x <= aabb.maxX && point.y >= aabb.minY && point.y <= aabb.maxY; 825 } 826}; 827
828// src/app/common/geometry/polygon-clamp.ts 829function findClosestPointOnPolygon(point, polygon) { 830 if (polygon.length < 3) { 831 return null; 832 } 833 let closestPoint = null; 834 let minDistance = Infinity; 835 let closestEdgeIndex = -1; 836 for (let i = 0; i < polygon.length; i++) { 837 const j = (i + 1) % polygon.length; 838 const edgeStart = polygon[i]; 839 const edgeEnd = polygon[j]; 840 const closest = projectPointOntoSegment(point, edgeStart, edgeEnd); 841 const distance = PointUtils.distance(point, closest); 842 if (distance < minDistance) { 843 minDistance = distance; 844 closestPoint = closest; 845 closestEdgeIndex = i; 846 } 847 } 848 if (!closestPoint) { 849 return null; 850 } 851 return { 852 point: closestPoint, 853 distance: minDistance, 854 edgeIndex: closestEdgeIndex 855 }; 856} 857function projectPointOntoSegment(point, segStart, segEnd) { 858 const dx = segEnd.x - segStart.x; 859 const dy = segEnd.y - segStart.y; 860 const lengthSq = dx * dx + dy * dy; 861 if (lengthSq === 0) { 862 return __spreadValues({}, segStart); 863 } 864 const t = Math.max(0, Math.min(1, ((point.x - segStart.x) * dx + (point.y - segStart.y) * dy) / lengthSq)); 865 return { 866 x: segStart.x + t * dx, 867 y: segStart.y + t * dy 868 }; 869} 870function getOutwardNormal(polygon, edgeIndex) { 871 const i = edgeIndex; 872 const j = (edgeIndex + 1) % polygon.length; 873 const edgeStart = polygon[i]; 874 const edgeEnd = polygon[j]; 875 const dx = edgeEnd.x - edgeStart.x; 876 const dy = edgeEnd.y - edgeStart.y; 877 const isClockwise = PointUtils.isClockwise(polygon); 878 let normalX; 879 let normalY; 880 if (isClockwise) { 881 normalX = dy; 882 normalY = -dx; 883 } else { 884 normalX = -dy; 885 normalY = dx; 886 } 887 const length = Math.sqrt(normalX * normalX + normalY * normalY); 888 if (length === 0) { 889 return { x: 0, y: 0 }; 890 } 891 return { 892 x: normalX / length, 893 y: normalY / length 894 }; 895} 896function clampPointOutsidePolygon(point, polygon, offsetDistance) { 897 if (polygon.length < 3) { 898 return point; 899 } 900 if (!PointUtils.pointInPolygon(point, polygon)) { 901 return point; 902 } 903 const closest = findClosestPointOnPolygon(point, polygon); 904 if (!closest) { 905 return point; 906 } 907 const normal = getOutwardNormal(polygon, closest.edgeIndex); 908 return { 909 x: closest.point.x + normal.x * offsetDistance, 910 y: closest.point.y + normal.y * offsetDistance 911 }; 912} 913var isPointInPolygon = PointUtils.pointInPolygon; 914
915// src/app/services/gps-location/gps-location.service.ts 916var _GPSLocationService = class _GPSLocationService { 917 get currentLocation() { 918 return this._location$.value; 919 } 920 get gpsTransform() { 921 return this._gpsTransform; 922 } 923 get buildingManager() { 924 return this.buildingDetectionManager; 925 } 926 constructor(gpsService, databaseService, settingsService, syncStateService) { 927 this.gpsService = gpsService; 928 this.databaseService = databaseService; 929 this.settingsService = settingsService; 930 this.syncStateService = syncStateService; 931 this._location$ = new BehaviorSubject(null); 932 this._gpsTransform = null; 933 this.transformReferencePoints = []; 934 this.campusTransformCandidates = []; 935 this.lastSyncStage = null; 936 this.buildingDetectionManager = new BuildingDetectionManager(); 937 this.currentCampus = null; 938 this.gpsPositionHandler = (position) => { 939 if (!position) { 940 return; 941 } 942 if (this.campusTransformCandidates.length === 0) { 943 this.clearCurrentCampusSelection(); 944 this._location$.next(null); 945 return; 946 } 947 const mapLocation = this.transformToMapLocation(position); 948 this._location$.next(mapLocation); 949 }; 950 this.location$ = new TrackedObservable((oldCount, newCount) => { 951 if (oldCount === 0 && newCount === 1) { 952 this.gpsPositionSubscription = this.gpsService.position$.subscribe(this.gpsPositionHandler); 953 } 954 }, (oldCount, newCount) => { 955 var _a; 956 if (oldCount === 1 && newCount === 0) { 957 (_a = this.gpsPositionSubscription) == null ? void 0 : _a.unsubscribe(); 958 this.gpsPositionSubscription = void 0; 959 } 960 }, this._location$); 961 void this.initialize(); 962 this.syncStateSubscription = this.syncStateService.state$.subscribe((state) => { 963 const stage = state.progress.stage; 964 if (this.lastSyncStage !== SyncStage.COMPLETED && stage === SyncStage.COMPLETED) { 965 void this.rebuildCampusTransforms(); 966 } 967 this.lastSyncStage = stage; 968 }); 969 } 970 initialize() { 971 return __async(this, null, function* () { 972 yield this.databaseService.onInitialized; 973 yield this.rebuildCampusTransforms(); 974 }); 975 } 976 rebuildCampusTransforms() { 977 return __async(this, null, function* () { 978 const campuses = this.databaseService.Campuses.filter((c) => !c.entity.isDeleted); 979 if (campuses.length === 0) { 980 this.clearTransformState(); 981 return; 982 } 983 const candidates = []; 984 for (const campus of campuses) { 985 const referencePoints = this.getCampusReferencePoints(campus); 986 if (referencePoints.length < 2) { 987 continue; 988 } 989 try { 990 const transform = createGPSTransform(referencePoints); 991 candidates.push({ 992 campus, 993 transform, 994 referencePoints, 995 bounds: this.getCampusBounds(campus, transform, referencePoints) 996 }); 997 } catch (err) { 998 console.error("[GPSLocationService]", "Failed to create GPS Transform:", err); 999 } 1000 } 1001 this.campusTransformCandidates = candidates; 1002 if (this.campusTransformCandidates.length === 0) { 1003 this.clearTransformState(); 1004 this._location$.next(null); 1005 return; 1006 } 1007 const currentPosition = this.gpsService.currentPosition; 1008 if (currentPosition) { 1009 this.gpsPositionHandler(currentPosition); 1010 } 1011 }); 1012 } 1013 clearTransformState() { 1014 this.campusTransformCandidates = []; 1015 this.clearCurrentCampusSelection(); 1016 } 1017 transformToMapLocation(position) { 1018 const gpsPoint = { latitude: position.latitude, longitude: position.longitude }; 1019 const selection = this.selectCampusForGPSPoint(gpsPoint); 1020 if (!selection) { 1021 this.clearCurrentCampusSelection(); 1022 return this.createNoCampusGpsLocation(position, gpsPoint); 1023 } 1024 this.applyCampusSelection(selection); 1025 try { 1026 const confidence = this.calculateConfidence(position); 1027 const mapPoint = transformGPSToMap(gpsPoint, selection.candidate.transform); 1028 const clampedPoint = this.clampGpsIfNeeded(mapPoint); 1029 const detection = this.buildingDetectionManager.detect(clampedPoint, this.currentCampus, position.altitude, position.altitudeAccuracy); 1030 return { 1031 point: clampedPoint, 1032 geographicPoint: gpsPoint, 1033 campus: this.currentCampus || void 0, 1034 building: detection.building || void 0, 1035 floor: detection.floor || void 0, 1036 confidence, 1037 accuracy: position.accuracy ? position.accuracy * 100 : 999 * 100,
1038 source: LocationSource.GPS, 1039 recordedAt: /* @__PURE__ */ new Date() 1040 }; 1041 } catch (error) { 1042 console.error("GPS transformation failed:", error); 1043 return null; 1044 } 1045 } 1046 createNoCampusGpsLocation(position, gpsPoint) { 1047 const confidence = this.calculateConfidence(position); 1048 return { 1049 point: transformFallbackGpsToMap(gpsPoint, RIGA_INITIAL_VIEW_CENTER), 1050 geographicPoint: gpsPoint, 1051 confidence, 1052 accuracy: position.accuracy ? position.accuracy * 100 : 999 * 100, 1053 source: LocationSource.GPS, 1054 recordedAt: /* @__PURE__ */ new Date() 1055 }; 1056 } 1057 getCampusReferencePoints(campus) { 1058 const referencePoints = []; 1059 for (const node of campus.nodes) { 1060 if (node.latitude != null && node.longitude != null && node.entity.locationX != null && node.entity.locationY != null) { 1061 referencePoints.push({ 1062 gps: { latitude: node.latitude, longitude: node.longitude }, 1063 map: { x: node.entity.locationX, y: node.entity.locationY }, 1064 id: node.entity.id 1065 }); 1066 } 1067 } 1068 return referencePoints; 1069 } 1070 getCampusBounds(campus, transform, referencePoints) { 1071 const { boundingBoxNorth: north, boundingBoxSouth: south, boundingBoxEast: east, boundingBoxWest: west } = campus; 1072 if (north != null && south != null && east != null && west != null && [north, south, east, west].every(Number.isFinite) && north >= south && east >= west && north <= 90 && south >= -90 && east <= 180 && west >= -180) { 1073 return { north, south, east, west }; 1074 } 1075 const points = referencePoints.map((reference) => reference.gps); 1076 const entities = [ 1077 ...campus.nodes, 1078 ...campus.floors.flatMap((floor) => floor.corners) 1079 ]; 1080 for (const model of entities) { 1081 const { locationX: x, locationY: y } = model.entity; 1082 if (x == null || y == null || !Number.isFinite(x) || !Number.isFinite(y)) 1083 continue; 1084 const point = transformMapToGPS({ x, y }, transform); 1085 if (Number.isFinite(point.latitude) && Number.isFinite(point.longitude)) 1086 points.push(point); 1087 } 1088 return { 1089 north: Math.max(...points.map((point) => point.latitude)), 1090 south: Math.min(...points.map((point) => point.latitude)), 1091 east: Math.max(...points.map((point) => point.longitude)), 1092 west: Math.min(...points.map((point) => point.longitude)) 1093 }; 1094 } 1095 isWithinCampusBounds(point, bounds) { 1096 const latitudeTolerance = _GPSLocationService.CAMPUS_BOUNDARY_TOLERANCE_METERS / 111320; 1097 const longitudeTolerance = latitudeTolerance / Math.max(0.01, Math.cos((bounds.north + bounds.south) / 2 * Math.PI / 180)); 1098 return point.latitude <= bounds.north + latitudeTolerance && point.latitude >= bounds.south - latitudeTolerance && point.longitude <= bounds.east + longitudeTolerance && point.longitude >= bounds.west - longitudeTolerance; 1099 } 1100 selectCampusForGPSPoint(gpsPoint) { 1101 let bestCandidate = null; 1102 let bestDistanceMeters = Number.POSITIVE_INFINITY; 1103 for (const candidate of this.campusTransformCandidates) { 1104 if (!this.isWithinCampusBounds(gpsPoint, candidate.bounds)) { 1105 continue; 1106 } 1107 const distanceMeters = this.getNearestReferenceDistanceMeters(gpsPoint, candidate.referencePoints); 1108 if (distanceMeters < bestDistanceMeters) { 1109 bestCandidate = candidate; 1110 bestDistanceMeters = distanceMeters; 1111 } 1112 } 1113 if (!bestCandidate) { 1114 return null; 1115 } 1116 return { 1117 candidate: bestCandidate, 1118 nearestReferenceDistanceMeters: bestDistanceMeters 1119 }; 1120 } 1121 applyCampusSelection(selection) { 1122 var _a; 1123 const selectedCampus = selection.candidate.campus; 1124 if (((_a = this.currentCampus) == null ? void 0 : _a.entity.id) !== selectedCampus.entity.id) { 1125 this.buildingDetectionManager.reset(); 1126 } 1127 this.currentCampus = selectedCampus; 1128 this._gpsTransform = selection.candidate.transform; 1129 this.transformReferencePoints = selection.candidate.referencePoints; 1130 } 1131 clearCurrentCampusSelection() { 1132 if (this.currentCampus || this._gpsTransform || this.transformReferencePoints.length > 0) { 1133 this.buildingDetectionManager.reset(); 1134 } 1135 this.currentCampus = null; 1136 this._gpsTransform = null; 1137 this.transformReferencePoints = []; 1138 } 1139 getNearestReferenceDistanceMeters(gpsPoint, referencePoints) { 1140 let nearestDistanceMeters = Number.POSITIVE_INFINITY; 1141 for (const referencePoint of referencePoints) { 1142 nearestDistanceMeters = Math.min(nearestDistanceMeters, this.getGPSDistanceMeters(gpsPoint, referencePoint.gps)); 1143 } 1144 return nearestDistanceMeters; 1145 } 1146 getGPSDistanceMeters(a, b) { 1147 const earthRadiusMeters = 6371e3; 1148 const lat1 = this.degreesToRadians(a.latitude); 1149 const lat2 = this.degreesToRadians(b.latitude); 1150 const deltaLat = this.degreesToRadians(b.latitude - a.latitude); 1151 const deltaLon = this.degreesToRadians(b.longitude - a.longitude); 1152 const haversine = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.
1152cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2); 1153 return 2 * earthRadiusMeters * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); 1154 } 1155 degreesToRadians(value) { 1156 return value * Math.PI / 180; 1157 } 1158 calculateConfidence(position) { 1159 if (position.accuracy === null) { 1160 return 0; 1161 } 1162 const minAcc = 5; 1163 const maxAcc = 40; 1164 const minQuality = 0; 1165 const maxQuality = 1; 1166 const acc = Math.max(position.accuracy, minAcc); 1167 const k = Math.log(maxQuality / 0.01) / (maxAcc - minAcc); 1168 const quality = maxQuality * Math.exp(-k * (acc - minAcc)); 1169 return Math.max(minQuality, Math.min(maxQuality, quality)); 1170 } 1171 /** 1172 * Clamps the GPS position to be outside building boundaries if enabled. 1173 * When the setting is enabled, finds the closest wall and places the position 1174 * 1 meter (100cm) outside that wall. 1175 * 1176 * @param point The GPS-derived map position to clamp 1177 * @returns The clamped position (outside buildings) or original if disabled/outside 1178 */ 1179 clampGpsIfNeeded(point) { 1180 if (!this.settingsService.currentSettings.clampGpsOutsideBuildings) { 1181 return point; 1182 } 1183 if (!this.currentCampus) { 1184 return point; 1185 } 1186 for (const building of this.currentCampus.buildings) { 1187 const groundFloor = building.floors.find((f) => f.entity.level === 1); 1188 if (!groundFloor || groundFloor.corners.length < 3) { 1189 continue; 1190 } 1191 const polygon = groundFloor.corners.map((c) => ({ 1192 x: c.entity.locationX || 0, 1193 y: c.entity.locationY || 0 1194 })); 1195 const clampedPoint = clampPointOutsidePolygon(point, polygon, 100); 1196 if (clampedPoint.x !== point.x || clampedPoint.y !== point.y) { 1197 return clampedPoint; 1198 } 1199 } 1200 return point; 1201 } 1202 ngOnDestroy() { 1203 var _a, _b; 1204 (_a = this.gpsPositionSubscription) == null ? void 0 : _a.unsubscribe(); 1205 this.gpsPositionSubscription = void 0; 1206 (_b = this.syncStateSubscription) == null ? void 0 : _b.unsubscribe(); 1207 this.syncStateSubscription = void 0; 1208 } 1209}; 1210_GPSLocationService.CAMPUS_BOUNDARY_TOLERANCE_METERS = 15; 1211_GPSLocationService.\u0275fac = function GPSLocationService_Factory(__ngFactoryType__) { 1212 return new (__ngFactoryType__ || _GPSLocationService)(\u0275\u0275inject(GPSService), \u0275\u0275inject(DatabaseService), \u0275\u0275inject(SettingsService), \u0275\u0275inject(SyncStateService)); 1213}; 1214_GPSLocationService.\u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _GPSLocationService, factory: _GPSLocationService.\u0275fac, providedIn: "root" }); 1215var GPSLocationService = _GPSLocationService; 1216(() => { 1217 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(GPSLocationService, [{ 1218 type: Injectable, 1219 args: [{ 1220 providedIn: "root" 1221 }] 1222 }], () => [{ type: GPSService }, { type: DatabaseService }, { type: SettingsService }, { type: SyncStateService }], null); 1223})(); 1224 1225export { 1226 createGPSTransform, 1227 transformGPSToMap, 1228 transformMapToGPS, 1229 PointUtils, 1230 RIGA_INITIAL_VIEW_CENTER, 1231 createFallbackGeoReference, 1232 transformFallbackGpsToMap, 1233 GPSLocationService 1234};
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.