@expofp/wayfinding 3.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +84 -0
  2. package/dist/core/createWayfindingEngine.d.ts +16 -0
  3. package/dist/core/createWayfindingEngine.js +87 -0
  4. package/dist/core/geometry/pointInPolygon.d.ts +17 -0
  5. package/dist/core/geometry/pointInPolygon.js +50 -0
  6. package/dist/core/geometry/projectPointOnSegment.d.ts +27 -0
  7. package/dist/core/geometry/projectPointOnSegment.js +21 -0
  8. package/dist/core/graph/buildGraph.d.ts +3 -0
  9. package/dist/core/graph/buildGraph.js +7 -0
  10. package/dist/core/graph/buildNGraph.d.ts +13 -0
  11. package/dist/core/graph/buildNGraph.js +24 -0
  12. package/dist/core/graph/constants.d.ts +20 -0
  13. package/dist/core/graph/constants.js +19 -0
  14. package/dist/core/graph/findShortestPath.d.ts +23 -0
  15. package/dist/core/graph/findShortestPath.js +65 -0
  16. package/dist/core/graph/graphCache.d.ts +7 -0
  17. package/dist/core/graph/graphCache.js +20 -0
  18. package/dist/core/graph/graphHelpers.d.ts +8 -0
  19. package/dist/core/graph/graphHelpers.js +52 -0
  20. package/dist/core/graph/linkCost.d.ts +13 -0
  21. package/dist/core/graph/linkCost.js +24 -0
  22. package/dist/core/graph/pathfinder/aStarPathFinder.d.ts +3 -0
  23. package/dist/core/graph/pathfinder/aStarPathFinder.js +64 -0
  24. package/dist/core/graph/pathfinder/parseNodeId.d.ts +10 -0
  25. package/dist/core/graph/pathfinder/parseNodeId.js +20 -0
  26. package/dist/core/index.d.ts +17 -0
  27. package/dist/core/index.js +12 -0
  28. package/dist/core/position/distanceToRoute.d.ts +3 -0
  29. package/dist/core/position/distanceToRoute.js +29 -0
  30. package/dist/core/position/gpsThreshold.d.ts +32 -0
  31. package/dist/core/position/gpsThreshold.js +48 -0
  32. package/dist/core/position/rerouteController.d.ts +13 -0
  33. package/dist/core/position/rerouteController.js +22 -0
  34. package/dist/core/position/snapToRoute.d.ts +9 -0
  35. package/dist/core/position/snapToRoute.js +50 -0
  36. package/dist/core/position/splitRouteByPoint.d.ts +6 -0
  37. package/dist/core/position/splitRouteByPoint.js +56 -0
  38. package/dist/core/rendering/computeTrailPoints.d.ts +12 -0
  39. package/dist/core/rendering/computeTrailPoints.js +35 -0
  40. package/dist/core/rendering/computeTransitionPoints.d.ts +32 -0
  41. package/dist/core/rendering/computeTransitionPoints.js +114 -0
  42. package/dist/core/rendering/getVisibleRouteLines.d.ts +10 -0
  43. package/dist/core/rendering/getVisibleRouteLines.js +18 -0
  44. package/dist/core/rendering/normalizeRouteDirection.d.ts +19 -0
  45. package/dist/core/rendering/normalizeRouteDirection.js +82 -0
  46. package/dist/core/rendering/routeGeometry.d.ts +9 -0
  47. package/dist/core/rendering/routeGeometry.js +10 -0
  48. package/dist/core/routing/buildMultiPointRoute.d.ts +13 -0
  49. package/dist/core/routing/buildMultiPointRoute.js +36 -0
  50. package/dist/core/routing/buildRoute.d.ts +12 -0
  51. package/dist/core/routing/buildRoute.js +19 -0
  52. package/dist/core/routing/getRouteLength.d.ts +3 -0
  53. package/dist/core/routing/getRouteLength.js +4 -0
  54. package/dist/core/routing/graphPointResolvers.d.ts +9 -0
  55. package/dist/core/routing/graphPointResolvers.js +13 -0
  56. package/dist/core/routing/optimizeWaypointOrder.d.ts +14 -0
  57. package/dist/core/routing/optimizeWaypointOrder.js +88 -0
  58. package/dist/core/routing/resolveWaypointCandidates.d.ts +23 -0
  59. package/dist/core/routing/resolveWaypointCandidates.js +49 -0
  60. package/dist/core/routing/routeResult.d.ts +4 -0
  61. package/dist/core/routing/routeResult.js +2 -0
  62. package/dist/core/types.d.ts +102 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/createWayfinding.d.ts +68 -0
  65. package/dist/createWayfinding.js +44 -0
  66. package/dist/index.d.ts +14 -0
  67. package/dist/index.js +13 -0
  68. package/dist/renderer/createWayfindingRenderer.d.ts +14 -0
  69. package/dist/renderer/createWayfindingRenderer.js +51 -0
  70. package/dist/renderer/iconManager.d.ts +39 -0
  71. package/dist/renderer/iconManager.js +166 -0
  72. package/dist/renderer/index.d.ts +3 -0
  73. package/dist/renderer/index.js +1 -0
  74. package/dist/renderer/layerManager.d.ts +27 -0
  75. package/dist/renderer/layerManager.js +40 -0
  76. package/dist/renderer/lineAnimation.d.ts +11 -0
  77. package/dist/renderer/lineAnimation.js +127 -0
  78. package/dist/renderer/routeLineManager.d.ts +44 -0
  79. package/dist/renderer/routeLineManager.js +62 -0
  80. package/dist/renderer/trailManager.d.ts +32 -0
  81. package/dist/renderer/trailManager.js +79 -0
  82. package/dist/renderer/types.d.ts +129 -0
  83. package/dist/renderer/types.js +1 -0
  84. package/dist/runtime/createWayfindingRuntime.d.ts +3 -0
  85. package/dist/runtime/createWayfindingRuntime.js +248 -0
  86. package/dist/runtime/endpointView.d.ts +20 -0
  87. package/dist/runtime/endpointView.js +40 -0
  88. package/dist/runtime/getRouteLines.d.ts +19 -0
  89. package/dist/runtime/getRouteLines.js +16 -0
  90. package/dist/runtime/index.d.ts +4 -0
  91. package/dist/runtime/index.js +2 -0
  92. package/dist/runtime/positionTrailView.d.ts +52 -0
  93. package/dist/runtime/positionTrailView.js +35 -0
  94. package/dist/runtime/positionView.d.ts +23 -0
  95. package/dist/runtime/positionView.js +25 -0
  96. package/dist/runtime/routeLinesView.d.ts +18 -0
  97. package/dist/runtime/routeLinesView.js +18 -0
  98. package/dist/runtime/routeRenderData.d.ts +31 -0
  99. package/dist/runtime/routeRenderData.js +46 -0
  100. package/dist/runtime/routeUpdate.d.ts +17 -0
  101. package/dist/runtime/routeUpdate.js +17 -0
  102. package/dist/runtime/snapPositionToRoute.d.ts +10 -0
  103. package/dist/runtime/snapPositionToRoute.js +36 -0
  104. package/dist/runtime/trailView.d.ts +20 -0
  105. package/dist/runtime/trailView.js +43 -0
  106. package/dist/runtime/transitionView.d.ts +17 -0
  107. package/dist/runtime/transitionView.js +50 -0
  108. package/dist/runtime/types.d.ts +86 -0
  109. package/dist/runtime/types.js +1 -0
  110. package/package.json +45 -0
@@ -0,0 +1,64 @@
1
+ import { lineAngle } from '@expofp/geometry';
2
+ import { buildNGraph } from '../buildNGraph.js';
3
+ import { TURN_PENALTY } from '../constants.js';
4
+ import aStarPathSearch from '../utils/a-star/a-star.js';
5
+ import { parseNodeId } from './parseNodeId.js';
6
+ function createAnglePenaltyDistance() {
7
+ return function anglePenaltyDistance(fromNode, toNode, link) {
8
+ let coef = 1;
9
+ if (toNode.parent) {
10
+ const [, parentX, parentY] = parseNodeId(toNode.parent.node.id);
11
+ const [, toX, toY] = parseNodeId(toNode.node.id);
12
+ const [, fromX, fromY] = parseNodeId(fromNode.id);
13
+ const previousAngle = lineAngle({ x: parentX, y: parentY }, { x: toX, y: toY });
14
+ const nextAngle = lineAngle({ x: toX, y: toY }, { x: fromX, y: fromY });
15
+ // Strict !== on floats is intentional: applies the 1% penalty for
16
+ // any non-zero direction change, no matter how small. This is legacy
17
+ // behavior — introducing an angle threshold would change route selection.
18
+ // lineAngle now returns radians (atan2) instead of degrees (law of
19
+ // cosines); identical inputs still yield identical bits, so the strict
20
+ // equality of two same-direction segments is preserved.
21
+ if (previousAngle !== nextAngle)
22
+ coef = TURN_PENALTY;
23
+ }
24
+ return coef * link.data.distance;
25
+ };
26
+ }
27
+ export function createAStarPathFinder() {
28
+ let finder = null;
29
+ let graph = null;
30
+ return {
31
+ build(lines, options) {
32
+ graph = buildNGraph(lines, options);
33
+ finder = aStarPathSearch(graph, {
34
+ oriented: options.oriented,
35
+ distance: createAnglePenaltyDistance(),
36
+ });
37
+ },
38
+ hasNode(id) {
39
+ if (!graph)
40
+ return false;
41
+ return !!graph.hasNode(id);
42
+ },
43
+ /**
44
+ * Returns path nodes in **source→target** order (`fromId` first, `toId`
45
+ * last). A* reconstructs from the goal, so the raw path is target→source; it
46
+ * is reversed here once, at the source, so no downstream reversal is needed.
47
+ * @param fromId
48
+ * @param toId
49
+ */
50
+ find(fromId, toId) {
51
+ if (!finder)
52
+ return [];
53
+ const nodes = finder.find(fromId, toId);
54
+ if (!nodes.length)
55
+ return [];
56
+ return nodes
57
+ .map((node) => {
58
+ const [layer, x, y] = parseNodeId(node.id);
59
+ return { id: node.id, layer, x, y };
60
+ })
61
+ .reverse();
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Decodes a node ID created by `pointId` back into [layer, x, y].
3
+ *
4
+ * Point IDs are formatted as "{layer}_{x}_{y}". Since layer names may
5
+ * contain underscores (e.g. "floor_1"), we parse from the right where
6
+ * x and y are always the last two segments.
7
+ * @param id
8
+ */
9
+ export declare function parseNodeId(id: string): [string, number, number];
10
+ //# sourceMappingURL=parseNodeId.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Decodes a node ID created by `pointId` back into [layer, x, y].
3
+ *
4
+ * Point IDs are formatted as "{layer}_{x}_{y}". Since layer names may
5
+ * contain underscores (e.g. "floor_1"), we parse from the right where
6
+ * x and y are always the last two segments.
7
+ * @param id
8
+ */
9
+ export function parseNodeId(id) {
10
+ const lastSep = id.lastIndexOf('_');
11
+ const secondLastSep = id.lastIndexOf('_', lastSep - 1);
12
+ if (secondLastSep === -1) {
13
+ throw new Error(`WF: invalid node ID format "${id}", expected "{layer}_{x}_{y}"`);
14
+ }
15
+ return [
16
+ id.substring(0, secondLastSep),
17
+ parseFloat(id.substring(secondLastSep + 1, lastSep)),
18
+ parseFloat(id.substring(lastSep + 1)),
19
+ ];
20
+ }
@@ -0,0 +1,17 @@
1
+ export type { Route, WayfindingEngine } from './createWayfindingEngine.js';
2
+ export { createWayfindingEngine } from './createWayfindingEngine.js';
3
+ export type { GraphDataSource, GraphLine, RouteEndpoint, RouteLine, RoutePoint, RouteSnapResult, SnapToRouteConfig, } from './types.js';
4
+ export type { SegmentProjection } from './geometry/projectPointOnSegment.js';
5
+ export { projectPointOnSegment } from './geometry/projectPointOnSegment.js';
6
+ export { computeTrailPoints } from './rendering/computeTrailPoints.js';
7
+ export type { TransitionPointDef } from './rendering/computeTransitionPoints.js';
8
+ export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
9
+ export { computeBoundingBox } from './rendering/routeGeometry.js';
10
+ export { getRouteLength } from './routing/getRouteLength.js';
11
+ export type { GpsProjectionConfig } from './position/gpsThreshold.js';
12
+ export { getThresholdFromMetersToSvg, getThresholdOrDefault, getThresholdSafe, resolveRerouteThresholdSvg, SNAP_THRESHOLD_METERS, } from './position/gpsThreshold.js';
13
+ export type { RerouteController } from './position/rerouteController.js';
14
+ export { createRerouteController } from './position/rerouteController.js';
15
+ export { snapToRoute } from './position/snapToRoute.js';
16
+ export { splitRouteByPoint } from './position/splitRouteByPoint.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,12 @@
1
+ export { createWayfindingEngine } from './createWayfindingEngine.js';
2
+ export { projectPointOnSegment } from './geometry/projectPointOnSegment.js';
3
+ // Rendering utilities
4
+ export { computeTrailPoints } from './rendering/computeTrailPoints.js';
5
+ export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
6
+ export { computeBoundingBox } from './rendering/routeGeometry.js';
7
+ // Routing utilities
8
+ export { getRouteLength } from './routing/getRouteLength.js';
9
+ export { getThresholdFromMetersToSvg, getThresholdOrDefault, getThresholdSafe, resolveRerouteThresholdSvg, SNAP_THRESHOLD_METERS, } from './position/gpsThreshold.js';
10
+ export { createRerouteController } from './position/rerouteController.js';
11
+ export { snapToRoute } from './position/snapToRoute.js';
12
+ export { splitRouteByPoint } from './position/splitRouteByPoint.js';
@@ -0,0 +1,3 @@
1
+ import type { RouteLine, RoutePoint } from '../types.js';
2
+ export declare function distancePointToRouteOnLayer(point: RoutePoint, lines: RouteLine[]): number;
3
+ //# sourceMappingURL=distanceToRoute.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { pointDistance } from '@expofp/geometry';
2
+ function distancePointToSegment(px, py, ax, ay, bx, by) {
3
+ const abx = bx - ax;
4
+ const aby = by - ay;
5
+ const apx = px - ax;
6
+ const apy = py - ay;
7
+ const abLen2 = abx * abx + aby * aby;
8
+ if (abLen2 === 0)
9
+ return pointDistance({ x: px, y: py }, { x: ax, y: ay });
10
+ let t = (apx * abx + apy * aby) / abLen2;
11
+ if (t < 0)
12
+ t = 0;
13
+ else if (t > 1)
14
+ t = 1;
15
+ const cx = ax + t * abx;
16
+ const cy = ay + t * aby;
17
+ return pointDistance({ x: px, y: py }, { x: cx, y: cy });
18
+ }
19
+ export function distancePointToRouteOnLayer(point, lines) {
20
+ let best = Infinity;
21
+ for (const line of lines) {
22
+ if (point.layer && (line.p0.layer !== point.layer || line.p1.layer !== point.layer))
23
+ continue;
24
+ const dist = distancePointToSegment(point.x, point.y, line.p0.x, line.p0.y, line.p1.x, line.p1.y);
25
+ if (dist < best)
26
+ best = dist;
27
+ }
28
+ return best;
29
+ }
@@ -0,0 +1,32 @@
1
+ export declare const SNAP_THRESHOLD_METERS = 7.5;
2
+ export interface GpsProjectionConfig {
3
+ readonly p0: {
4
+ x: number;
5
+ y: number;
6
+ lat: number;
7
+ lng: number;
8
+ };
9
+ readonly p2: {
10
+ x: number;
11
+ y: number;
12
+ lat: number;
13
+ lng: number;
14
+ };
15
+ }
16
+ export declare const getThresholdSafe: (value?: number) => number | undefined;
17
+ export declare const getThresholdOrDefault: (value: number | undefined, defaultValue: number) => number;
18
+ export declare function getThresholdFromMetersToSvg({ thresholdInMeters, gpsConfig, }: {
19
+ thresholdInMeters?: number;
20
+ gpsConfig?: GpsProjectionConfig;
21
+ }): number;
22
+ /**
23
+ * Resolves the reroute threshold (SVG units) for the active plan: honours
24
+ * `gpsConfig.rerouteThreshold` (meters) override, otherwise falls back to
25
+ * {@link REROUTE_THRESHOLD_METERS}. Returns plan-scaled SVG units suitable
26
+ * for `shouldReroute`.
27
+ * @param gpsConfig
28
+ */
29
+ export declare function resolveRerouteThresholdSvg(gpsConfig?: GpsProjectionConfig & {
30
+ rerouteThreshold?: number;
31
+ }): number;
32
+ //# sourceMappingURL=gpsThreshold.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { haversineDistance, pointDistance } from '@expofp/geometry';
2
+ export const SNAP_THRESHOLD_METERS = 7.5;
3
+ // Reroute must be strictly larger than snap.
4
+ const REROUTE_THRESHOLD_METERS = 15;
5
+ const FALLBACK_SVG_THRESHOLD = 20;
6
+ function metersToSvgUnits(meters, gpsConfig) {
7
+ const { p0, p2 } = gpsConfig;
8
+ const fullGpsDistance = haversineDistance(p0, p2);
9
+ const fullSvgLength = pointDistance(p0, p2);
10
+ if (!fullGpsDistance || !fullSvgLength)
11
+ return 0;
12
+ return meters * (fullSvgLength / fullGpsDistance);
13
+ }
14
+ export const getThresholdSafe = (value) => {
15
+ if (typeof value === 'number' && Number.isFinite(value)) {
16
+ return value;
17
+ }
18
+ return;
19
+ };
20
+ export const getThresholdOrDefault = (value, defaultValue) => {
21
+ const threshold = getThresholdSafe(value);
22
+ if (threshold === undefined || threshold === 0) {
23
+ return defaultValue;
24
+ }
25
+ return threshold;
26
+ };
27
+ export function getThresholdFromMetersToSvg({ thresholdInMeters, gpsConfig, }) {
28
+ let thresholdSvg = 0;
29
+ // 0 / missing meters → "not configured" → fall back to FALLBACK_SVG_THRESHOLD.
30
+ if (thresholdInMeters && gpsConfig) {
31
+ thresholdSvg = metersToSvgUnits(thresholdInMeters, gpsConfig);
32
+ }
33
+ if (!thresholdSvg) {
34
+ thresholdSvg = FALLBACK_SVG_THRESHOLD;
35
+ }
36
+ return thresholdSvg;
37
+ }
38
+ /**
39
+ * Resolves the reroute threshold (SVG units) for the active plan: honours
40
+ * `gpsConfig.rerouteThreshold` (meters) override, otherwise falls back to
41
+ * {@link REROUTE_THRESHOLD_METERS}. Returns plan-scaled SVG units suitable
42
+ * for `shouldReroute`.
43
+ * @param gpsConfig
44
+ */
45
+ export function resolveRerouteThresholdSvg(gpsConfig) {
46
+ const thresholdInMeters = getThresholdOrDefault(gpsConfig?.rerouteThreshold, REROUTE_THRESHOLD_METERS);
47
+ return getThresholdFromMetersToSvg({ thresholdInMeters, gpsConfig });
48
+ }
@@ -0,0 +1,13 @@
1
+ import type { RouteLine, RoutePoint } from '../types.js';
2
+ /** Decides whether the current GPS tick warrants rebuilding the route. */
3
+ export interface RerouteController {
4
+ /** Evaluates the current tick. Returns `true` when the caller should rebuild the route. */
5
+ shouldReroute(position: RoutePoint, routeLines: RouteLine[]): boolean;
6
+ }
7
+ /**
8
+ * `thresholdSvg` — plan-scale-aware off-route distance (SVG units), produced by
9
+ * `resolveRerouteThresholdSvg` and passed in by the composition root.
10
+ * @param thresholdSvg
11
+ */
12
+ export declare function createRerouteController(thresholdSvg: number): RerouteController;
13
+ //# sourceMappingURL=rerouteController.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { distancePointToRouteOnLayer } from './distanceToRoute.js';
2
+ /**
3
+ * `thresholdSvg` — plan-scale-aware off-route distance (SVG units), produced by
4
+ * `resolveRerouteThresholdSvg` and passed in by the composition root.
5
+ * @param thresholdSvg
6
+ */
7
+ export function createRerouteController(thresholdSvg) {
8
+ return {
9
+ shouldReroute(position, routeLines) {
10
+ if (!routeLines.length)
11
+ return false;
12
+ const positionLayer = position.layer;
13
+ // Layer mismatch — route doesn't touch CP's layer; fire without the distance check.
14
+ if (positionLayer &&
15
+ !routeLines.some((line) => line.p0.layer === positionLayer && line.p1.layer === positionLayer)) {
16
+ return true;
17
+ }
18
+ const distance = distancePointToRouteOnLayer(position, routeLines);
19
+ return distance > thresholdSvg;
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,9 @@
1
+ import { type RouteLine, type RoutePoint, type RouteSnapResult, type SnapToRouteConfig } from '../types.js';
2
+ export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
3
+ /**
4
+ * Snap a position onto a from→to route polyline. Pure geometry: nearest segment
5
+ * within `snapThreshold`, plus the remaining arc-length to the destination.
6
+ * Segments on a layer other than `position.layer` are skipped.
7
+ */
8
+ export declare function snapToRoute(position: RoutePoint, routeLines: readonly RouteLine[], { snapThreshold }: SnapToRouteConfig): RouteSnapResult;
9
+ //# sourceMappingURL=snapToRoute.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { pointDistance } from '@expofp/geometry';
2
+ import { projectPointOnSegment } from '../geometry/projectPointOnSegment.js';
3
+ export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
4
+ /**
5
+ * Snap a position onto a from→to route polyline. Pure geometry: nearest segment
6
+ * within `snapThreshold`, plus the remaining arc-length to the destination.
7
+ * Segments on a layer other than `position.layer` are skipped.
8
+ */
9
+ export function snapToRoute(position, routeLines, { snapThreshold }) {
10
+ const lineCount = routeLines.length;
11
+ if (!lineCount)
12
+ return { snapped: false };
13
+ const posLayer = position.layer || null;
14
+ let bestDistance = Infinity;
15
+ let best = null;
16
+ let accumulatedLength = 0;
17
+ let hasSegmentOnPosLayer = posLayer == null;
18
+ for (let i = 0; i < lineCount; i++) {
19
+ const line = routeLines[i];
20
+ const { p0, p1 } = line;
21
+ const segLen = pointDistance(p0, p1);
22
+ if (segLen === 0)
23
+ continue;
24
+ if (posLayer != null && String(p0.layer) !== posLayer) {
25
+ accumulatedLength += segLen;
26
+ continue;
27
+ }
28
+ hasSegmentOnPosLayer = true;
29
+ const projection = projectPointOnSegment({ x: position.x, y: position.y }, p0, p1);
30
+ if (projection.distance < bestDistance) {
31
+ bestDistance = projection.distance;
32
+ best = {
33
+ point: { layer: p0.layer, x: projection.projected.x, y: projection.projected.y },
34
+ line,
35
+ fromStart: accumulatedLength + segLen * projection.t,
36
+ };
37
+ }
38
+ accumulatedLength += segLen;
39
+ }
40
+ if (posLayer != null && !hasSegmentOnPosLayer)
41
+ return { snapped: false };
42
+ if (!best || bestDistance > snapThreshold)
43
+ return { snapped: false };
44
+ return {
45
+ snapped: true,
46
+ snappedPoint: best.point,
47
+ snappedLine: best.line,
48
+ distance: accumulatedLength - best.fromStart,
49
+ };
50
+ }
@@ -0,0 +1,6 @@
1
+ import { type RouteLine, type RoutePoint } from '../types.js';
2
+ export declare function splitRouteByPoint(routeLines: RouteLine[], snapPoint: RoutePoint): {
3
+ passed: RouteLine[];
4
+ remaining: RouteLine[];
5
+ };
6
+ //# sourceMappingURL=splitRouteByPoint.d.ts.map
@@ -0,0 +1,56 @@
1
+ export function splitRouteByPoint(routeLines, snapPoint) {
2
+ const EPS = 1e-6;
3
+ let bestIndex = -1;
4
+ let bestT = 0;
5
+ let bestDist = Infinity;
6
+ for (let i = 0; i < routeLines.length; i++) {
7
+ const line = routeLines[i];
8
+ if (line.p0.layer !== snapPoint.layer)
9
+ continue;
10
+ const dx = line.p1.x - line.p0.x;
11
+ const dy = line.p1.y - line.p0.y;
12
+ const len2 = dx * dx + dy * dy;
13
+ if (len2 === 0)
14
+ continue;
15
+ const t = ((snapPoint.x - line.p0.x) * dx + (snapPoint.y - line.p0.y) * dy) / len2;
16
+ if (t < -EPS || t > 1 + EPS)
17
+ continue;
18
+ const clampedT = Math.min(1, Math.max(0, t));
19
+ const px = line.p0.x + clampedT * dx;
20
+ const py = line.p0.y + clampedT * dy;
21
+ const dist = (snapPoint.x - px) ** 2 + (snapPoint.y - py) ** 2;
22
+ if (dist < bestDist) {
23
+ bestDist = dist;
24
+ bestIndex = i;
25
+ bestT = clampedT;
26
+ }
27
+ }
28
+ if (bestIndex === -1) {
29
+ return {
30
+ passed: [],
31
+ remaining: routeLines,
32
+ };
33
+ }
34
+ if (bestT < EPS) {
35
+ return {
36
+ passed: routeLines.slice(0, bestIndex),
37
+ remaining: routeLines.slice(bestIndex),
38
+ };
39
+ }
40
+ if (bestT > 1 - EPS) {
41
+ return {
42
+ passed: routeLines.slice(0, bestIndex + 1),
43
+ remaining: routeLines.slice(bestIndex + 1),
44
+ };
45
+ }
46
+ const line = routeLines[bestIndex];
47
+ const splitPoint = {
48
+ layer: line.p0.layer,
49
+ x: line.p0.x + bestT * (line.p1.x - line.p0.x),
50
+ y: line.p0.y + bestT * (line.p1.y - line.p0.y),
51
+ };
52
+ return {
53
+ passed: [...routeLines.slice(0, bestIndex), { ...line, p1: splitPoint }],
54
+ remaining: [{ ...line, p0: splitPoint }, ...routeLines.slice(bestIndex + 1)],
55
+ };
56
+ }
@@ -0,0 +1,12 @@
1
+ import { type Point2Like } from '@expofp/geometry';
2
+ /**
3
+ * Generates evenly-spaced points along a quadratic Bézier curve
4
+ * between two points. Used for dotted trail visualization when
5
+ * a point (kiosk, GPS, marker, etc.) is far from the route.
6
+ * @param from - Start point (e.g. route endpoint)
7
+ * @param to - End point (e.g. off-route position)
8
+ * @param interval - Spacing between points in SVG units (default: 8)
9
+ * @param curvature - Arc height as fraction of distance (0 = straight, 0.4 = default arc)
10
+ */
11
+ export declare function computeTrailPoints(from: Point2Like, to: Point2Like, interval?: number, curvature?: number): Point2Like[];
12
+ //# sourceMappingURL=computeTrailPoints.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { pointDistance } from '@expofp/geometry';
2
+ const DEFAULT_CURVATURE = 0.4;
3
+ const DEFAULT_INTERVAL = 8;
4
+ /**
5
+ * Generates evenly-spaced points along a quadratic Bézier curve
6
+ * between two points. Used for dotted trail visualization when
7
+ * a point (kiosk, GPS, marker, etc.) is far from the route.
8
+ * @param from - Start point (e.g. route endpoint)
9
+ * @param to - End point (e.g. off-route position)
10
+ * @param interval - Spacing between points in SVG units (default: 8)
11
+ * @param curvature - Arc height as fraction of distance (0 = straight, 0.4 = default arc)
12
+ */
13
+ export function computeTrailPoints(from, to, interval = DEFAULT_INTERVAL, curvature = DEFAULT_CURVATURE) {
14
+ const distance = pointDistance(to, from);
15
+ if (distance === 0 || interval <= 0)
16
+ return [];
17
+ const count = Math.max(Math.ceil(distance / interval) + 1, 2);
18
+ const mid = {
19
+ x: (from.x + to.x) / 2,
20
+ y: (from.y + to.y) / 2 - distance * curvature,
21
+ };
22
+ const points = [];
23
+ for (let i = 0; i < count; i++) {
24
+ const t = i / (count - 1);
25
+ points.push(quadraticBezier(from, mid, to, t));
26
+ }
27
+ return points;
28
+ }
29
+ function quadraticBezier(p0, p1, p2, t) {
30
+ const mt = 1 - t;
31
+ return {
32
+ x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
33
+ y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y,
34
+ };
35
+ }
@@ -0,0 +1,32 @@
1
+ import { type RouteLine } from '../types.js';
2
+ interface TransitionPointsInput {
3
+ /** Full route including virtual lines */
4
+ readonly routeLines: RouteLine[];
5
+ /** Floor to scope to. `null` = no floor scoping (returns transitions on every floor). */
6
+ readonly currentLayerName: string | null;
7
+ /** Floor names ordered low → high (from layerStore.floors) */
8
+ readonly floorOrder: string[];
9
+ }
10
+ export interface TransitionPointDef {
11
+ readonly x: number;
12
+ readonly y: number;
13
+ /** Floor where the icon is rendered */
14
+ readonly layer: string;
15
+ /** Floor the transition leads to (for click-to-switch) */
16
+ readonly targetLayer: string;
17
+ readonly direction: 'up' | 'down' | 'neutral';
18
+ /** Whether this is an entry (arriving from another floor) or exit (leaving to another floor) in walking direction */
19
+ readonly role: 'entry' | 'exit';
20
+ }
21
+ /**
22
+ * Computes deduplicated, correctly-directed transition points for the
23
+ * current floor.
24
+ *
25
+ * Works directly with the full `routeLines` array (including virtual lines)
26
+ * and does NOT depend on `routeNextFloor` — the target floor is read from
27
+ * the opposite endpoint of each virtual line.
28
+ * @param input
29
+ */
30
+ export declare function computeTransitionPoints(input: TransitionPointsInput): TransitionPointDef[];
31
+ export {};
32
+ //# sourceMappingURL=computeTransitionPoints.d.ts.map
@@ -0,0 +1,114 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Helpers
3
+ // ---------------------------------------------------------------------------
4
+ function layersMatch(a, b) {
5
+ return a.toLowerCase() === b.toLowerCase();
6
+ }
7
+ function findFloorIndex(floorOrder, layerName) {
8
+ return floorOrder.findIndex((name) => layersMatch(name, layerName));
9
+ }
10
+ function resolveDirection(floorOrder, currentLayer, targetLayer) {
11
+ const currentIndex = findFloorIndex(floorOrder, currentLayer);
12
+ const targetIndex = findFloorIndex(floorOrder, targetLayer);
13
+ if (currentIndex === -1 || targetIndex === -1)
14
+ return 'neutral';
15
+ return targetIndex > currentIndex ? 'up' : 'down';
16
+ }
17
+ function findNextVirtualLink(crossFloorLines, fromLayer, visited) {
18
+ return crossFloorLines.find((line) => {
19
+ if (layersMatch(line.p0.layer, fromLayer))
20
+ return !visited.has(line.p1.layer.toLowerCase());
21
+ if (layersMatch(line.p1.layer, fromLayer))
22
+ return !visited.has(line.p0.layer.toLowerCase());
23
+ return false;
24
+ });
25
+ }
26
+ /**
27
+ * Follows a chain of virtual lines through transit floors (floors with no
28
+ * physical route lines) until reaching a floor that has physical lines.
29
+ *
30
+ * Example: route L1 → L2(transit) → L3 — immediateTarget is L2,
31
+ * but L2 has no physical lines, so we follow the virtual chain to L3.
32
+ * @param crossFloorLines
33
+ * @param floorsWithPhysicalLines
34
+ * @param currentLayer
35
+ * @param immediateTarget
36
+ */
37
+ function resolveTransitTarget(crossFloorLines, floorsWithPhysicalLines, currentLayer, immediateTarget) {
38
+ if (floorsWithPhysicalLines.has(immediateTarget.toLowerCase())) {
39
+ return immediateTarget;
40
+ }
41
+ let target = immediateTarget;
42
+ const visited = new Set([currentLayer.toLowerCase(), target.toLowerCase()]);
43
+ let next = findNextVirtualLink(crossFloorLines, target, visited);
44
+ while (next) {
45
+ target = layersMatch(next.p0.layer, target) ? next.p1.layer : next.p0.layer;
46
+ if (floorsWithPhysicalLines.has(target.toLowerCase()))
47
+ break;
48
+ visited.add(target.toLowerCase());
49
+ next = findNextVirtualLink(crossFloorLines, target, visited);
50
+ }
51
+ return target;
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Main
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Computes deduplicated, correctly-directed transition points for the
58
+ * current floor.
59
+ *
60
+ * Works directly with the full `routeLines` array (including virtual lines)
61
+ * and does NOT depend on `routeNextFloor` — the target floor is read from
62
+ * the opposite endpoint of each virtual line.
63
+ * @param input
64
+ */
65
+ export function computeTransitionPoints(input) {
66
+ const { routeLines, currentLayerName, floorOrder } = input;
67
+ // 1. Only virtual lines that actually cross floors
68
+ const crossFloorLines = routeLines.filter((line) => line.virtual && !layersMatch(line.p0.layer, line.p1.layer));
69
+ // 2. Floors that have physical (non-virtual) route lines — used to skip transit floors
70
+ const floorsWithPhysicalLines = new Set();
71
+ for (const line of routeLines) {
72
+ if (!line.virtual) {
73
+ floorsWithPhysicalLines.add(line.p0.layer.toLowerCase());
74
+ floorsWithPhysicalLines.add(line.p1.layer.toLowerCase());
75
+ }
76
+ }
77
+ // 3. Keep only lines touching the current floor (when set)
78
+ const relevantLines = currentLayerName
79
+ ? crossFloorLines.filter((line) => layersMatch(line.p0.layer, currentLayerName) ||
80
+ layersMatch(line.p1.layer, currentLayerName))
81
+ : crossFloorLines;
82
+ // 4. Pick the endpoint on the current floor; the other is the target
83
+ const points = relevantLines.map((line) => {
84
+ const pickP0 = currentLayerName ? layersMatch(line.p0.layer, currentLayerName) : true;
85
+ const visible = pickP0 ? line.p0 : line.p1;
86
+ const target = pickP0 ? line.p1 : line.p0;
87
+ // Follow virtual chain through transit floors (e.g. L1→L2(transit)→L3 → targetLayer=L3)
88
+ const resolvedTarget = resolveTransitTarget(crossFloorLines, floorsWithPhysicalLines, visible.layer, target.layer);
89
+ return {
90
+ x: visible.x,
91
+ y: visible.y,
92
+ layer: visible.layer,
93
+ targetLayer: resolvedTarget,
94
+ direction: resolveDirection(floorOrder, visible.layer, resolvedTarget),
95
+ // Current-floor endpoint at `p0` → route departs here → "exit" (clickable
96
+ // to advance); at `p1` → arrives here → "entry". Null layer → default "exit".
97
+ role: currentLayerName ? (pickP0 ? 'exit' : 'entry') : 'exit',
98
+ };
99
+ });
100
+ // 5. Deduplicate by coordinates. Drop entries that collide with an exit
101
+ // at the same node so the directional transition_up/down icon isn't
102
+ // overridden by transition_entry in multi-point routes.
103
+ const exitKeys = new Set(points.filter((p) => p.role === 'exit').map((p) => `${p.x}:${p.y}`));
104
+ const emittedKeys = new Set();
105
+ return points.filter((point) => {
106
+ const key = `${point.x}:${point.y}`;
107
+ if (point.role === 'entry' && exitKeys.has(key))
108
+ return false;
109
+ if (emittedKeys.has(key))
110
+ return false;
111
+ emittedKeys.add(key);
112
+ return true;
113
+ });
114
+ }
@@ -0,0 +1,10 @@
1
+ import { type RouteLine } from '../types.js';
2
+ /**
3
+ * Filters route lines for a given floor: removes virtual segments
4
+ * and keeps only lines on the specified floor.
5
+ * @param routeLines - All route lines (may span multiple floors)
6
+ * @param currentFloor - Floor to filter by, or `null` to return all non-virtual lines
7
+ * (single-floor plans / "show all" mode)
8
+ */
9
+ export declare function getVisibleRouteLines(routeLines: readonly RouteLine[], currentFloor: string | null): RouteLine[];
10
+ //# sourceMappingURL=getVisibleRouteLines.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Filters route lines for a given floor: removes virtual segments
3
+ * and keeps only lines on the specified floor.
4
+ * @param routeLines - All route lines (may span multiple floors)
5
+ * @param currentFloor - Floor to filter by, or `null` to return all non-virtual lines
6
+ * (single-floor plans / "show all" mode)
7
+ */
8
+ export function getVisibleRouteLines(routeLines, currentFloor) {
9
+ const result = [];
10
+ for (const line of routeLines) {
11
+ if (line.virtual)
12
+ continue;
13
+ if (currentFloor !== null && line.p0.layer !== currentFloor)
14
+ continue;
15
+ result.push(line);
16
+ }
17
+ return result;
18
+ }
@@ -0,0 +1,19 @@
1
+ import type { RouteLine, RoutePoint } from '../types.js';
2
+ /**
3
+ * Ensures that `routeLines` run in the from→to direction.
4
+ *
5
+ * A* returns paths in target→source order. For rendering (arrow animation,
6
+ * route progress split) and remaining-distance computation the lines must
7
+ * go from the origin to the destination.
8
+ *
9
+ * Direction is decided by projecting `from` (or `to` as a fallback) onto
10
+ * `routeLines`: if the anchor lands closer to the polyline end than to
11
+ * its start, the array is reversed and each segment's endpoints are
12
+ * flipped to keep continuity.
13
+ * @param routeLines Lines to normalize.
14
+ * @param from Route origin (user-selected "from" point).
15
+ * @param to Route destination (user-selected "to" point).
16
+ * @returns Lines guaranteed to run from→to, or the original array unchanged.
17
+ */
18
+ export declare function normalizeRouteDirection(routeLines: RouteLine[], from: RoutePoint, to: RoutePoint): RouteLine[];
19
+ //# sourceMappingURL=normalizeRouteDirection.d.ts.map