@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.
- package/README.md +84 -0
- package/dist/core/createWayfindingEngine.d.ts +16 -0
- package/dist/core/createWayfindingEngine.js +87 -0
- package/dist/core/geometry/pointInPolygon.d.ts +17 -0
- package/dist/core/geometry/pointInPolygon.js +50 -0
- package/dist/core/geometry/projectPointOnSegment.d.ts +27 -0
- package/dist/core/geometry/projectPointOnSegment.js +21 -0
- package/dist/core/graph/buildGraph.d.ts +3 -0
- package/dist/core/graph/buildGraph.js +7 -0
- package/dist/core/graph/buildNGraph.d.ts +13 -0
- package/dist/core/graph/buildNGraph.js +24 -0
- package/dist/core/graph/constants.d.ts +20 -0
- package/dist/core/graph/constants.js +19 -0
- package/dist/core/graph/findShortestPath.d.ts +23 -0
- package/dist/core/graph/findShortestPath.js +65 -0
- package/dist/core/graph/graphCache.d.ts +7 -0
- package/dist/core/graph/graphCache.js +20 -0
- package/dist/core/graph/graphHelpers.d.ts +8 -0
- package/dist/core/graph/graphHelpers.js +52 -0
- package/dist/core/graph/linkCost.d.ts +13 -0
- package/dist/core/graph/linkCost.js +24 -0
- package/dist/core/graph/pathfinder/aStarPathFinder.d.ts +3 -0
- package/dist/core/graph/pathfinder/aStarPathFinder.js +64 -0
- package/dist/core/graph/pathfinder/parseNodeId.d.ts +10 -0
- package/dist/core/graph/pathfinder/parseNodeId.js +20 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +12 -0
- package/dist/core/position/distanceToRoute.d.ts +3 -0
- package/dist/core/position/distanceToRoute.js +29 -0
- package/dist/core/position/gpsThreshold.d.ts +32 -0
- package/dist/core/position/gpsThreshold.js +48 -0
- package/dist/core/position/rerouteController.d.ts +13 -0
- package/dist/core/position/rerouteController.js +22 -0
- package/dist/core/position/snapToRoute.d.ts +9 -0
- package/dist/core/position/snapToRoute.js +50 -0
- package/dist/core/position/splitRouteByPoint.d.ts +6 -0
- package/dist/core/position/splitRouteByPoint.js +56 -0
- package/dist/core/rendering/computeTrailPoints.d.ts +12 -0
- package/dist/core/rendering/computeTrailPoints.js +35 -0
- package/dist/core/rendering/computeTransitionPoints.d.ts +32 -0
- package/dist/core/rendering/computeTransitionPoints.js +114 -0
- package/dist/core/rendering/getVisibleRouteLines.d.ts +10 -0
- package/dist/core/rendering/getVisibleRouteLines.js +18 -0
- package/dist/core/rendering/normalizeRouteDirection.d.ts +19 -0
- package/dist/core/rendering/normalizeRouteDirection.js +82 -0
- package/dist/core/rendering/routeGeometry.d.ts +9 -0
- package/dist/core/rendering/routeGeometry.js +10 -0
- package/dist/core/routing/buildMultiPointRoute.d.ts +13 -0
- package/dist/core/routing/buildMultiPointRoute.js +36 -0
- package/dist/core/routing/buildRoute.d.ts +12 -0
- package/dist/core/routing/buildRoute.js +19 -0
- package/dist/core/routing/getRouteLength.d.ts +3 -0
- package/dist/core/routing/getRouteLength.js +4 -0
- package/dist/core/routing/graphPointResolvers.d.ts +9 -0
- package/dist/core/routing/graphPointResolvers.js +13 -0
- package/dist/core/routing/optimizeWaypointOrder.d.ts +14 -0
- package/dist/core/routing/optimizeWaypointOrder.js +88 -0
- package/dist/core/routing/resolveWaypointCandidates.d.ts +23 -0
- package/dist/core/routing/resolveWaypointCandidates.js +49 -0
- package/dist/core/routing/routeResult.d.ts +4 -0
- package/dist/core/routing/routeResult.js +2 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/core/types.js +1 -0
- package/dist/createWayfinding.d.ts +68 -0
- package/dist/createWayfinding.js +44 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/renderer/createWayfindingRenderer.d.ts +14 -0
- package/dist/renderer/createWayfindingRenderer.js +51 -0
- package/dist/renderer/iconManager.d.ts +39 -0
- package/dist/renderer/iconManager.js +166 -0
- package/dist/renderer/index.d.ts +3 -0
- package/dist/renderer/index.js +1 -0
- package/dist/renderer/layerManager.d.ts +27 -0
- package/dist/renderer/layerManager.js +40 -0
- package/dist/renderer/lineAnimation.d.ts +11 -0
- package/dist/renderer/lineAnimation.js +127 -0
- package/dist/renderer/routeLineManager.d.ts +44 -0
- package/dist/renderer/routeLineManager.js +62 -0
- package/dist/renderer/trailManager.d.ts +32 -0
- package/dist/renderer/trailManager.js +79 -0
- package/dist/renderer/types.d.ts +129 -0
- package/dist/renderer/types.js +1 -0
- package/dist/runtime/createWayfindingRuntime.d.ts +3 -0
- package/dist/runtime/createWayfindingRuntime.js +248 -0
- package/dist/runtime/endpointView.d.ts +20 -0
- package/dist/runtime/endpointView.js +40 -0
- package/dist/runtime/getRouteLines.d.ts +19 -0
- package/dist/runtime/getRouteLines.js +16 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/positionTrailView.d.ts +52 -0
- package/dist/runtime/positionTrailView.js +35 -0
- package/dist/runtime/positionView.d.ts +23 -0
- package/dist/runtime/positionView.js +25 -0
- package/dist/runtime/routeLinesView.d.ts +18 -0
- package/dist/runtime/routeLinesView.js +18 -0
- package/dist/runtime/routeRenderData.d.ts +31 -0
- package/dist/runtime/routeRenderData.js +46 -0
- package/dist/runtime/routeUpdate.d.ts +17 -0
- package/dist/runtime/routeUpdate.js +17 -0
- package/dist/runtime/snapPositionToRoute.d.ts +10 -0
- package/dist/runtime/snapPositionToRoute.js +36 -0
- package/dist/runtime/trailView.d.ts +20 -0
- package/dist/runtime/trailView.js +43 -0
- package/dist/runtime/transitionView.d.ts +17 -0
- package/dist/runtime/transitionView.js +50 -0
- package/dist/runtime/types.d.ts +86 -0
- package/dist/runtime/types.js +1 -0
- 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,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,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
|