PageSourceSearch

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

js stridenavigation.com collected 2026-09-24 14:54:16 UTC 42,174 bytes, 1,234 lines download raw bytes

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.