@zephytiju/prism-geovision-embed 0.1.0
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 +101 -0
- package/dist/EmbedBody.d.ts +17 -0
- package/dist/EmbedBody.d.ts.map +1 -0
- package/dist/EmbedBody.js +81 -0
- package/dist/GeovisionEmbed.d.ts +20 -0
- package/dist/GeovisionEmbed.d.ts.map +1 -0
- package/dist/GeovisionEmbed.js +44 -0
- package/dist/describeViews.d.ts +26 -0
- package/dist/describeViews.d.ts.map +1 -0
- package/dist/describeViews.js +59 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/locales/en.json +29 -0
- package/dist/locales/index.d.ts +79 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/index.js +18 -0
- package/dist/locales/zh-CN.json +29 -0
- package/dist/map/MiniMap.d.ts +21 -0
- package/dist/map/MiniMap.d.ts.map +1 -0
- package/dist/map/MiniMap.js +75 -0
- package/dist/map/ReplayControl.d.ts +20 -0
- package/dist/map/ReplayControl.d.ts.map +1 -0
- package/dist/map/ReplayControl.js +37 -0
- package/dist/map/heat.d.ts +41 -0
- package/dist/map/heat.d.ts.map +1 -0
- package/dist/map/heat.js +54 -0
- package/dist/map/projection.d.ts +30 -0
- package/dist/map/projection.d.ts.map +1 -0
- package/dist/map/projection.js +54 -0
- package/dist/map/replay.d.ts +52 -0
- package/dist/map/replay.d.ts.map +1 -0
- package/dist/map/replay.js +91 -0
- package/dist/map/sideColoring.d.ts +39 -0
- package/dist/map/sideColoring.d.ts.map +1 -0
- package/dist/map/sideColoring.js +69 -0
- package/dist/map/tokens.d.ts +10 -0
- package/dist/map/tokens.d.ts.map +1 -0
- package/dist/map/tokens.js +25 -0
- package/dist/rail/LayerRailCompact.d.ts +19 -0
- package/dist/rail/LayerRailCompact.d.ts.map +1 -0
- package/dist/rail/LayerRailCompact.js +85 -0
- package/dist/resolveView.d.ts +18 -0
- package/dist/resolveView.d.ts.map +1 -0
- package/dist/resolveView.js +58 -0
- package/dist/seed.d.ts +22 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +45 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/locales/en.json +29 -0
- package/locales/zh-CN.json +29 -0
- package/package.json +68 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { boundsFor, boundsDiagonalDegrees, graticuleLines, project } from "./projection.js";
|
|
4
|
+
import { heatBlobs } from "./heat.js";
|
|
5
|
+
import { paintFor, withinSideIndices } from "./sideColoring.js";
|
|
6
|
+
import { buildTracks, hasMotion, positionAt } from "./replay.js";
|
|
7
|
+
import { cssTokenColor } from "./tokens.js";
|
|
8
|
+
/**
|
|
9
|
+
* The simplified STATIC EQUIRECTANGULAR mini-map: an SVG surface over the
|
|
10
|
+
* snapshot's located entities — normal markers from the overlay layer,
|
|
11
|
+
* kernel-density blobs from each enabled heat layer, side-color mapping
|
|
12
|
+
* applied to marker fills/outlines, and (when the replay is scrubbed) the
|
|
13
|
+
* entities at their interpolated track positions with the static positions
|
|
14
|
+
* faded underneath. Display-only; pointer capture stays on the slot.
|
|
15
|
+
*/
|
|
16
|
+
export function MiniMap({ file, container, rail, replayFraction, replayWindow, strings, }) {
|
|
17
|
+
const items = useMemo(() => file.snapshot.entityResults?.items ?? [], [file]);
|
|
18
|
+
const located = useMemo(() => items.filter((item) => item.lat !== undefined && item.lon !== undefined), [items]);
|
|
19
|
+
const camera = file.view.camera;
|
|
20
|
+
const bounds = useMemo(() => boundsFor(located, { lat: camera.lat, lon: camera.lon }), [located, camera]);
|
|
21
|
+
const diagonal = boundsDiagonalDegrees(bounds);
|
|
22
|
+
const { rows, columns } = graticuleLines(bounds);
|
|
23
|
+
const enabled = (kind) => rail.find((layer) => layer.kind === kind && layer.enabled) ?? null;
|
|
24
|
+
const sideLayer = file.view.layers.find((layer) => layer.kind === "side-color");
|
|
25
|
+
const sidePaints = useMemo(() => {
|
|
26
|
+
if (sideLayer?.kind !== "side-color" || enabled("side-color") === null)
|
|
27
|
+
return new Map();
|
|
28
|
+
const indices = withinSideIndices(items.map((item) => ({ id: item.id, type: item.type })), sideLayer);
|
|
29
|
+
const paints = new Map();
|
|
30
|
+
items.forEach((item) => {
|
|
31
|
+
paints.set(item.id, paintFor({ type: item.type }, sideLayer, indices.get(item.id) ?? 0));
|
|
32
|
+
});
|
|
33
|
+
return paints;
|
|
34
|
+
}, [sideLayer, items, rail]);
|
|
35
|
+
const blobs = useMemo(() => {
|
|
36
|
+
const layer = enabled("heat");
|
|
37
|
+
const heatLayers = file.view.layers.filter((candidate) => candidate.kind === "heat");
|
|
38
|
+
const target = heatLayers.find((candidate) => candidate.kind === "heat" && layer !== null) ?? heatLayers[0];
|
|
39
|
+
if (layer === null || target === undefined || target.kind !== "heat")
|
|
40
|
+
return [];
|
|
41
|
+
return heatBlobs(located, target).map((blob) => ({ ...blob, intensity: layer.opacity }));
|
|
42
|
+
}, [file, located, rail]);
|
|
43
|
+
const tracks = useMemo(() => buildTracks(located, file.snapshot.timelineEvents, replayWindow), [located, file, replayWindow]);
|
|
44
|
+
const motion = hasMotion(tracks);
|
|
45
|
+
const W = 320;
|
|
46
|
+
const H = 170;
|
|
47
|
+
const toXY = (point) => {
|
|
48
|
+
const p = project(point, bounds);
|
|
49
|
+
return { x: p.x * W, y: p.y * H };
|
|
50
|
+
};
|
|
51
|
+
return (_jsxs("svg", { viewBox: `0 0 ${W} ${H}`, style: { width: "100%", height: "100%", display: "block" }, role: "img", "aria-label": strings.mapAria, "data-testid": "geovision-embed-map", children: [_jsx("rect", { x: 0, y: 0, width: W, height: H, fill: "var(--mantine-color-deep-filled, #0b0f16)" }), rows.map((row) => (_jsx("line", { x1: 0, y1: row * H, x2: W, y2: row * H, stroke: "#1c2533", strokeWidth: 0.5 }, `r${String(row)}`))), columns.map((column) => (_jsx("line", { x1: column * W, y1: 0, x2: column * W, y2: H, stroke: "#1c2533", strokeWidth: 0.5 }, `c${String(column)}`))), blobs.map((blob, index) => {
|
|
52
|
+
const { x, y } = toXY(blob);
|
|
53
|
+
const radius = 10 + blob.density * 26;
|
|
54
|
+
return (_jsx("circle", { cx: x, cy: y, r: radius, fill: cssTokenColor(container, "threat"), opacity: blob.density * blob.intensity * 0.55, "data-testid": "geovision-embed-heat-blob" }, `blob-${String(index)}`));
|
|
55
|
+
}), motion
|
|
56
|
+
? located.map((item) => {
|
|
57
|
+
const track = tracks.find((candidate) => candidate.entityId === item.id);
|
|
58
|
+
if (track === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
const { x, y } = toXY(positionAt(track, replayFraction, replayWindow));
|
|
61
|
+
return (_jsx("circle", { cx: toXY(item).x, cy: toXY(item).y, r: 2, fill: "#4a5a70", opacity: 0.6 }, `ghost-${item.id}`));
|
|
62
|
+
})
|
|
63
|
+
: null, located.map((item) => {
|
|
64
|
+
const track = tracks.find((candidate) => candidate.entityId === item.id);
|
|
65
|
+
const position = motion && track !== undefined ? positionAt(track, replayFraction, replayWindow) : item;
|
|
66
|
+
const { x, y } = toXY(position);
|
|
67
|
+
const paint = sidePaints.get(item.id) ?? null;
|
|
68
|
+
const selected = file.workspace.selectedEntity === item.id;
|
|
69
|
+
const fill = cssTokenColor(container, paint?.fillToken ?? "marker");
|
|
70
|
+
const outline = paint?.outlineToken !== undefined && paint?.outlineToken !== null
|
|
71
|
+
? cssTokenColor(container, paint.outlineToken)
|
|
72
|
+
: null;
|
|
73
|
+
return (_jsxs("g", { "data-testid": "geovision-embed-marker", "data-entity-id": item.id, "data-token": paint?.fillToken ?? "marker", children: [selected ? _jsx("circle", { cx: x, cy: y, r: 7, fill: "none", stroke: cssTokenColor(container, "accent"), strokeWidth: 1.2 }) : null, outline !== null ? (_jsx("circle", { cx: x, cy: y, r: 5.5, fill: "none", stroke: outline, strokeWidth: 1.4 })) : null, _jsx("circle", { cx: x, cy: y, r: 3.4, fill: fill }), _jsx("text", { x: x + 6, y: y + 3, fontSize: 7, fill: "#a7b4c6", fontFamily: "var(--mantine-font-family-monospace, monospace)", children: item.label })] }, item.id));
|
|
74
|
+
}), _jsx("text", { x: 6, y: H - 6, fontSize: 7, fill: "#5d6b7e", fontFamily: "var(--mantine-font-family-monospace, monospace)", children: `${bounds.minLon.toFixed(1)}° … ${bounds.maxLon.toFixed(1)}° / diag ${diagonal.toFixed(1)}°` })] }));
|
|
75
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReplayWindow } from "./replay.js";
|
|
2
|
+
import type { GeovisionEmbedStrings } from "../locales/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The VIDEO-LIKE replay control: play/pause + a progress scrub over the
|
|
5
|
+
* snapshot's time-windowed events. Scrob positions are derived state
|
|
6
|
+
* (display-only) — nothing is published and nothing navigates away. The
|
|
7
|
+
* play loop advances the fraction on a wall-clock interval, stopping at the
|
|
8
|
+
* live edge (fraction 1).
|
|
9
|
+
*/
|
|
10
|
+
export declare function ReplayControl({ fraction, playing, disabled, timeWindow, strings, onFraction, onTogglePlay, }: {
|
|
11
|
+
readonly fraction: number;
|
|
12
|
+
readonly playing: boolean;
|
|
13
|
+
/** Static frames disable the control entirely (no pointer capture). */
|
|
14
|
+
readonly disabled: boolean;
|
|
15
|
+
readonly timeWindow: ReplayWindow;
|
|
16
|
+
readonly strings: GeovisionEmbedStrings;
|
|
17
|
+
readonly onFraction: (fraction: number) => void;
|
|
18
|
+
readonly onTogglePlay: () => void;
|
|
19
|
+
}): React.ReactElement;
|
|
20
|
+
//# sourceMappingURL=ReplayControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplayControl.d.ts","sourceRoot":"","sources":["../../src/map/ReplayControl.tsx"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAKjE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,UAAU,EACV,OAAO,EACP,UAAU,EACV,YAAY,GACb,EAAE;IACD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC;CACnC,GAAG,KAAK,CAAC,YAAY,CA0DrB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { replayTimeLabel } from "./replay.js";
|
|
4
|
+
/** Fraction advanced per tick (~8.3s for a full sweep at 120ms ticks). */
|
|
5
|
+
const PLAY_STEP = 0.014;
|
|
6
|
+
/**
|
|
7
|
+
* The VIDEO-LIKE replay control: play/pause + a progress scrub over the
|
|
8
|
+
* snapshot's time-windowed events. Scrob positions are derived state
|
|
9
|
+
* (display-only) — nothing is published and nothing navigates away. The
|
|
10
|
+
* play loop advances the fraction on a wall-clock interval, stopping at the
|
|
11
|
+
* live edge (fraction 1).
|
|
12
|
+
*/
|
|
13
|
+
export function ReplayControl({ fraction, playing, disabled, timeWindow, strings, onFraction, onTogglePlay, }) {
|
|
14
|
+
const fractionRef = useRef(fraction);
|
|
15
|
+
fractionRef.current = fraction;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!playing || disabled)
|
|
18
|
+
return;
|
|
19
|
+
const handle = window.setInterval(() => {
|
|
20
|
+
onFraction(Math.min(1, fractionRef.current + PLAY_STEP)); // parent stops at the edge
|
|
21
|
+
}, 120);
|
|
22
|
+
return () => {
|
|
23
|
+
window.clearInterval(handle);
|
|
24
|
+
};
|
|
25
|
+
}, [playing, disabled, onFraction]);
|
|
26
|
+
return (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "3px 8px", borderTop: "1px solid var(--mantine-color-border-filled)" }, "data-testid": "geovision-embed-replay", children: [_jsx("button", { type: "button", "data-testid": "geovision-embed-replay-toggle", "aria-label": playing ? strings.replayPause : strings.replayPlay, disabled: disabled, onClick: onTogglePlay, style: {
|
|
27
|
+
fontSize: 10,
|
|
28
|
+
padding: "1px 8px",
|
|
29
|
+
cursor: disabled ? "default" : "pointer",
|
|
30
|
+
background: "var(--mantine-color-input-filled)",
|
|
31
|
+
color: "var(--mantine-color-text-filled)",
|
|
32
|
+
border: "1px solid var(--mantine-color-border-filled)",
|
|
33
|
+
borderRadius: 4,
|
|
34
|
+
}, children: playing ? "❚❚" : "▶" }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: fraction, disabled: disabled, "aria-label": strings.replayScrub, "data-testid": "geovision-embed-replay-scrub", onChange: (event) => {
|
|
35
|
+
onFraction(Number(event.target.value));
|
|
36
|
+
}, style: { flex: 1, height: 14 } }), _jsx("span", { "data-testid": "geovision-embed-replay-time", style: { fontSize: 9, fontFamily: "var(--mantine-font-family-monospace)", color: "var(--mantine-color-muted-filled)" }, children: `${strings.replayTime} ${replayTimeLabel(fraction, timeWindow)}` })] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified KERNEL-DENSITY blobs for the embed's SVG mini-map (pure): each
|
|
3
|
+
* located entity splats a quadratic kernel of the layer's radius (meters →
|
|
4
|
+
* degrees via the equirectangular approximation) over its neighbours, and
|
|
5
|
+
* the accumulated density scales a blurred blob's radius/alpha. This is the
|
|
6
|
+
* compact embed variant of the globe's full heat-grid kernel
|
|
7
|
+
* (PrismGlobeViewportMicroUI src/model/heat.ts) — same selection semantics,
|
|
8
|
+
* blob output instead of an offscreen canvas texture.
|
|
9
|
+
*/
|
|
10
|
+
import type { GeovisionHeatLayer } from "@zephytiju/lattice-common-bundle/geovisionView";
|
|
11
|
+
/** One rendered blob. */
|
|
12
|
+
export interface HeatBlob {
|
|
13
|
+
readonly lat: number;
|
|
14
|
+
readonly lon: number;
|
|
15
|
+
/** Relative density 0..1 (normalized over the layer's blobs). */
|
|
16
|
+
readonly density: number;
|
|
17
|
+
}
|
|
18
|
+
export interface HeatSelectionFilter {
|
|
19
|
+
readonly entityKinds: readonly string[];
|
|
20
|
+
readonly tags: readonly string[];
|
|
21
|
+
readonly eventKinds: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** True when the item matches the heat selection (kinds OR tags — stream types are the item `type`). */
|
|
24
|
+
export declare function matchesSelection(entity: {
|
|
25
|
+
readonly type: string;
|
|
26
|
+
readonly tags?: readonly string[];
|
|
27
|
+
}, filter: HeatSelectionFilter): boolean;
|
|
28
|
+
/** Kernel weight of `distanceDegrees` under `radiusDegrees` (quadratic falloff). */
|
|
29
|
+
export declare function kernelWeight(distanceDegrees: number, radiusDegrees: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* Derives the layer's blobs: every located entity matching the selection
|
|
32
|
+
* accumulates kernel weight from its matching neighbours; density is
|
|
33
|
+
* normalized to 0..1 against the strongest blob.
|
|
34
|
+
*/
|
|
35
|
+
export declare function heatBlobs(entities: readonly {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly type: string;
|
|
38
|
+
readonly lat?: number;
|
|
39
|
+
readonly lon?: number;
|
|
40
|
+
}[], layer: GeovisionHeatLayer): readonly HeatBlob[];
|
|
41
|
+
//# sourceMappingURL=heat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heat.d.ts","sourceRoot":"","sources":["../../src/map/heat.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gDAAgD,CAAC;AAKzF,yBAAyB;AACzB,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wGAAwG;AACxG,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EACpE,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAKT;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAInF;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,SAAS;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EACjH,KAAK,EAAE,kBAAkB,GACxB,SAAS,QAAQ,EAAE,CAyBrB"}
|
package/dist/map/heat.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified KERNEL-DENSITY blobs for the embed's SVG mini-map (pure): each
|
|
3
|
+
* located entity splats a quadratic kernel of the layer's radius (meters →
|
|
4
|
+
* degrees via the equirectangular approximation) over its neighbours, and
|
|
5
|
+
* the accumulated density scales a blurred blob's radius/alpha. This is the
|
|
6
|
+
* compact embed variant of the globe's full heat-grid kernel
|
|
7
|
+
* (PrismGlobeViewportMicroUI src/model/heat.ts) — same selection semantics,
|
|
8
|
+
* blob output instead of an offscreen canvas texture.
|
|
9
|
+
*/
|
|
10
|
+
const KM_PER_DEGREE = 111;
|
|
11
|
+
/** True when the item matches the heat selection (kinds OR tags — stream types are the item `type`). */
|
|
12
|
+
export function matchesSelection(entity, filter) {
|
|
13
|
+
const byKind = filter.entityKinds.length > 0 && filter.entityKinds.includes(entity.type);
|
|
14
|
+
const entityTags = entity.tags ?? [];
|
|
15
|
+
const byTag = filter.tags.length > 0 && filter.tags.some((tag) => entityTags.includes(tag));
|
|
16
|
+
return byKind || byTag;
|
|
17
|
+
}
|
|
18
|
+
/** Kernel weight of `distanceDegrees` under `radiusDegrees` (quadratic falloff). */
|
|
19
|
+
export function kernelWeight(distanceDegrees, radiusDegrees) {
|
|
20
|
+
if (distanceDegrees >= radiusDegrees || radiusDegrees <= 0)
|
|
21
|
+
return 0;
|
|
22
|
+
const ratio = distanceDegrees / radiusDegrees;
|
|
23
|
+
return (1 - ratio * ratio) ** 2;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derives the layer's blobs: every located entity matching the selection
|
|
27
|
+
* accumulates kernel weight from its matching neighbours; density is
|
|
28
|
+
* normalized to 0..1 against the strongest blob.
|
|
29
|
+
*/
|
|
30
|
+
export function heatBlobs(entities, layer) {
|
|
31
|
+
const radiusDegrees = Math.max(0.02, (layer.radius / 1000) / KM_PER_DEGREE);
|
|
32
|
+
const located = entities.filter((entity) => matchesSelection(entity, layer.selection.filter) &&
|
|
33
|
+
entity.lat !== undefined &&
|
|
34
|
+
entity.lon !== undefined);
|
|
35
|
+
const scored = located.map((entity) => {
|
|
36
|
+
let weight = kernelWeight(0, radiusDegrees); // self weight
|
|
37
|
+
for (const other of located) {
|
|
38
|
+
if (other.id === entity.id)
|
|
39
|
+
continue;
|
|
40
|
+
const dLat = other.lat - entity.lat;
|
|
41
|
+
const dLon = other.lon - entity.lon;
|
|
42
|
+
weight += kernelWeight(Math.sqrt(dLat * dLat + dLon * dLon), radiusDegrees);
|
|
43
|
+
}
|
|
44
|
+
return { lat: entity.lat, lon: entity.lon, weight };
|
|
45
|
+
});
|
|
46
|
+
const max = scored.reduce((best, blob) => Math.max(best, blob.weight), 0);
|
|
47
|
+
if (max <= 0)
|
|
48
|
+
return [];
|
|
49
|
+
return scored.map((blob) => ({
|
|
50
|
+
lat: blob.lat,
|
|
51
|
+
lon: blob.lon,
|
|
52
|
+
density: blob.weight / max,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static equirectangular mini-map projection (pure math): linear lon/lat →
|
|
3
|
+
* x/y over a fit-to-content bounds derived from the snapshot's located
|
|
4
|
+
* entities (plus the saved camera as a fallback anchor). No tiles, no WebGL
|
|
5
|
+
* — the embed map is a display-only SVG surface.
|
|
6
|
+
*/
|
|
7
|
+
export interface GeoPoint {
|
|
8
|
+
readonly lat: number;
|
|
9
|
+
readonly lon: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MapBounds {
|
|
12
|
+
readonly minLat: number;
|
|
13
|
+
readonly maxLat: number;
|
|
14
|
+
readonly minLon: number;
|
|
15
|
+
readonly maxLon: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function boundsFor(points: readonly GeoPoint[], fallback: GeoPoint): MapBounds;
|
|
18
|
+
/** Projects a lon/lat into the [0..1]² unit square of the bounds (clamped). */
|
|
19
|
+
export declare function project(point: GeoPoint, bounds: MapBounds): {
|
|
20
|
+
readonly x: number;
|
|
21
|
+
readonly y: number;
|
|
22
|
+
};
|
|
23
|
+
/** Degree length of the bounds' diagonal (used to scale blob radii). */
|
|
24
|
+
export declare function boundsDiagonalDegrees(bounds: MapBounds): number;
|
|
25
|
+
/** Graticule line fractions for the bounds (roughly 4 lat / 4 lon lines). */
|
|
26
|
+
export declare function graticuleLines(bounds: MapBounds): {
|
|
27
|
+
readonly rows: readonly number[];
|
|
28
|
+
readonly columns: readonly number[];
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/map/projection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAoBpF;AAED,+EAA+E;AAC/E,wBAAgB,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAG;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAMtG;AAED,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAI/D;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG;IACjD,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CASA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static equirectangular mini-map projection (pure math): linear lon/lat →
|
|
3
|
+
* x/y over a fit-to-content bounds derived from the snapshot's located
|
|
4
|
+
* entities (plus the saved camera as a fallback anchor). No tiles, no WebGL
|
|
5
|
+
* — the embed map is a display-only SVG surface.
|
|
6
|
+
*/
|
|
7
|
+
/** Geographic padding (degrees) around the content bounds. */
|
|
8
|
+
const PAD_DEGREES = 0.35;
|
|
9
|
+
export function boundsFor(points, fallback) {
|
|
10
|
+
if (points.length === 0) {
|
|
11
|
+
return { minLat: fallback.lat - 1, maxLat: fallback.lat + 1, minLon: fallback.lon - 1, maxLon: fallback.lon + 1 };
|
|
12
|
+
}
|
|
13
|
+
let minLat = points[0].lat;
|
|
14
|
+
let maxLat = points[0].lat;
|
|
15
|
+
let minLon = points[0].lon;
|
|
16
|
+
let maxLon = points[0].lon;
|
|
17
|
+
for (const point of points) {
|
|
18
|
+
minLat = Math.min(minLat, point.lat);
|
|
19
|
+
maxLat = Math.max(maxLat, point.lat);
|
|
20
|
+
minLon = Math.min(minLon, point.lon);
|
|
21
|
+
maxLon = Math.max(maxLon, point.lon);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
minLat: minLat - PAD_DEGREES,
|
|
25
|
+
maxLat: maxLat + PAD_DEGREES,
|
|
26
|
+
minLon: minLon - PAD_DEGREES,
|
|
27
|
+
maxLon: maxLon + PAD_DEGREES,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Projects a lon/lat into the [0..1]² unit square of the bounds (clamped). */
|
|
31
|
+
export function project(point, bounds) {
|
|
32
|
+
const lonSpan = Math.max(1e-6, bounds.maxLon - bounds.minLon);
|
|
33
|
+
const latSpan = Math.max(1e-6, bounds.maxLat - bounds.minLat);
|
|
34
|
+
const cx = (Math.min(bounds.maxLon, Math.max(bounds.minLon, point.lon)) - bounds.minLon) / lonSpan;
|
|
35
|
+
const cy = (bounds.maxLat - Math.min(bounds.maxLat, Math.max(bounds.minLat, point.lat))) / latSpan;
|
|
36
|
+
return { x: cx, y: cy };
|
|
37
|
+
}
|
|
38
|
+
/** Degree length of the bounds' diagonal (used to scale blob radii). */
|
|
39
|
+
export function boundsDiagonalDegrees(bounds) {
|
|
40
|
+
const dLat = bounds.maxLat - bounds.minLat;
|
|
41
|
+
const dLon = bounds.maxLon - bounds.minLon;
|
|
42
|
+
return Math.sqrt(dLat * dLat + dLon * dLon);
|
|
43
|
+
}
|
|
44
|
+
/** Graticule line fractions for the bounds (roughly 4 lat / 4 lon lines). */
|
|
45
|
+
export function graticuleLines(bounds) {
|
|
46
|
+
const rows = [];
|
|
47
|
+
const columns = [];
|
|
48
|
+
for (let i = 1; i < 5; i += 1) {
|
|
49
|
+
rows.push(i / 5);
|
|
50
|
+
columns.push(i / 5);
|
|
51
|
+
}
|
|
52
|
+
void bounds;
|
|
53
|
+
return { rows, columns };
|
|
54
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIDEO-LIKE REPLAY math (pure): per-entity movement tracks derived from the
|
|
3
|
+
* snapshot's time-windowed timeline events, scrubbed by a 0..1 progress
|
|
4
|
+
* fraction across the window. Display-only — the replay sets nothing global
|
|
5
|
+
* and publishes nothing.
|
|
6
|
+
*
|
|
7
|
+
* TRACK DERIVATION (documented): the file's TimelineEventItem shape carries
|
|
8
|
+
* no coordinates, so a track's waypoints place the entity at its SNAPSHOT
|
|
9
|
+
* position plus deterministic micro-offsets (an index-scaled alternating
|
|
10
|
+
* spiral — no RNG, so every surface derives identical motion from the same
|
|
11
|
+
* file). Between adjacent waypoints the scrub position interpolates
|
|
12
|
+
* LINEARLY BY TIME; entities without events stay fixed at their snapshot
|
|
13
|
+
* position.
|
|
14
|
+
*/
|
|
15
|
+
import type { TimelineEventItem } from "@zephytiju/lattice-common-interfaces";
|
|
16
|
+
import type { GeoPoint } from "./projection.js";
|
|
17
|
+
/** One waypoint of an entity's movement track. */
|
|
18
|
+
export interface ReplayWaypoint {
|
|
19
|
+
/** Elapsed ms since the window start. */
|
|
20
|
+
readonly at: number;
|
|
21
|
+
readonly lat: number;
|
|
22
|
+
readonly lon: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ReplayTrack {
|
|
25
|
+
readonly entityId: string;
|
|
26
|
+
readonly waypoints: readonly ReplayWaypoint[];
|
|
27
|
+
}
|
|
28
|
+
export interface ReplayWindow {
|
|
29
|
+
readonly startMs: number;
|
|
30
|
+
readonly endMs: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds the movement tracks for the located entities from the window's
|
|
34
|
+
* events (events for OTHER subjects are ignored; unlocated entities get no
|
|
35
|
+
* track and stay fixed).
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildTracks(entities: readonly {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly lat?: number;
|
|
40
|
+
readonly lon?: number;
|
|
41
|
+
}[], events: readonly TimelineEventItem[], window: ReplayWindow): readonly ReplayTrack[];
|
|
42
|
+
/**
|
|
43
|
+
* Position of a track at a scrub fraction (0..1 of the window): piecewise
|
|
44
|
+
* LINEAR interpolation between the surrounding waypoints BY TIME; clamped at
|
|
45
|
+
* both ends.
|
|
46
|
+
*/
|
|
47
|
+
export declare function positionAt(track: ReplayTrack, fraction: number, window: ReplayWindow): GeoPoint;
|
|
48
|
+
/** True when the events give any entity a multi-waypoint track. */
|
|
49
|
+
export declare function hasMotion(tracks: readonly ReplayTrack[]): boolean;
|
|
50
|
+
/** UTC hh:mm label for a scrub fraction of the window. */
|
|
51
|
+
export declare function replayTimeLabel(fraction: number, window: ReplayWindow): string;
|
|
52
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/map/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CAC/C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AASD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,SAAS;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EAC1F,MAAM,EAAE,SAAS,iBAAiB,EAAE,EACpC,MAAM,EAAE,YAAY,GACnB,SAAS,WAAW,EAAE,CAsBxB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,QAAQ,CAsB/F;AAED,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAEjE;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAO9E"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIDEO-LIKE REPLAY math (pure): per-entity movement tracks derived from the
|
|
3
|
+
* snapshot's time-windowed timeline events, scrubbed by a 0..1 progress
|
|
4
|
+
* fraction across the window. Display-only — the replay sets nothing global
|
|
5
|
+
* and publishes nothing.
|
|
6
|
+
*
|
|
7
|
+
* TRACK DERIVATION (documented): the file's TimelineEventItem shape carries
|
|
8
|
+
* no coordinates, so a track's waypoints place the entity at its SNAPSHOT
|
|
9
|
+
* position plus deterministic micro-offsets (an index-scaled alternating
|
|
10
|
+
* spiral — no RNG, so every surface derives identical motion from the same
|
|
11
|
+
* file). Between adjacent waypoints the scrub position interpolates
|
|
12
|
+
* LINEARLY BY TIME; entities without events stay fixed at their snapshot
|
|
13
|
+
* position.
|
|
14
|
+
*/
|
|
15
|
+
/** Deterministic micro-offset (degrees) for waypoint index i (1-based steps). */
|
|
16
|
+
function offsetFor(index) {
|
|
17
|
+
const magnitude = 0.045 * Math.sqrt(index);
|
|
18
|
+
const angle = index * 2.399963; // golden-angle spiral, radians
|
|
19
|
+
return { dLat: magnitude * Math.sin(angle), dLon: magnitude * Math.cos(angle) };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds the movement tracks for the located entities from the window's
|
|
23
|
+
* events (events for OTHER subjects are ignored; unlocated entities get no
|
|
24
|
+
* track and stay fixed).
|
|
25
|
+
*/
|
|
26
|
+
export function buildTracks(entities, events, window) {
|
|
27
|
+
const tracks = [];
|
|
28
|
+
for (const entity of entities) {
|
|
29
|
+
if (entity.lat === undefined || entity.lon === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const baseLat = entity.lat;
|
|
32
|
+
const baseLon = entity.lon;
|
|
33
|
+
const own = events
|
|
34
|
+
.filter((event) => event.subjectId === entity.id)
|
|
35
|
+
.slice()
|
|
36
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
37
|
+
const waypoints = [
|
|
38
|
+
{ at: 0, lat: baseLat, lon: baseLon }, // window start: snapshot position
|
|
39
|
+
];
|
|
40
|
+
own.forEach((event, index) => {
|
|
41
|
+
const at = Date.parse(event.at) - window.startMs;
|
|
42
|
+
if (!Number.isFinite(at) || at <= 0)
|
|
43
|
+
return;
|
|
44
|
+
const { dLat, dLon } = offsetFor(index + 1);
|
|
45
|
+
waypoints.push({ at, lat: baseLat + dLat, lon: baseLon + dLon });
|
|
46
|
+
});
|
|
47
|
+
tracks.push({ entityId: entity.id, waypoints });
|
|
48
|
+
}
|
|
49
|
+
return tracks;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Position of a track at a scrub fraction (0..1 of the window): piecewise
|
|
53
|
+
* LINEAR interpolation between the surrounding waypoints BY TIME; clamped at
|
|
54
|
+
* both ends.
|
|
55
|
+
*/
|
|
56
|
+
export function positionAt(track, fraction, window) {
|
|
57
|
+
const clamped = Math.min(1, Math.max(0, fraction));
|
|
58
|
+
const elapsed = clamped * (window.endMs - window.startMs);
|
|
59
|
+
const points = track.waypoints;
|
|
60
|
+
const first = points[0];
|
|
61
|
+
if (points.length === 1 || elapsed <= first.at) {
|
|
62
|
+
return { lat: first.lat, lon: first.lon };
|
|
63
|
+
}
|
|
64
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
65
|
+
const previous = points[i - 1];
|
|
66
|
+
const current = points[i];
|
|
67
|
+
if (elapsed <= current.at) {
|
|
68
|
+
const span = current.at - previous.at;
|
|
69
|
+
const t = span <= 0 ? 1 : (elapsed - previous.at) / span;
|
|
70
|
+
return {
|
|
71
|
+
lat: previous.lat + (current.lat - previous.lat) * t,
|
|
72
|
+
lon: previous.lon + (current.lon - previous.lon) * t,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const last = points[points.length - 1];
|
|
77
|
+
return { lat: last.lat, lon: last.lon };
|
|
78
|
+
}
|
|
79
|
+
/** True when the events give any entity a multi-waypoint track. */
|
|
80
|
+
export function hasMotion(tracks) {
|
|
81
|
+
return tracks.some((track) => track.waypoints.length > 1);
|
|
82
|
+
}
|
|
83
|
+
/** UTC hh:mm label for a scrub fraction of the window. */
|
|
84
|
+
export function replayTimeLabel(fraction, window) {
|
|
85
|
+
const clamped = Math.min(1, Math.max(0, fraction));
|
|
86
|
+
const ms = window.startMs + clamped * (window.endMs - window.startMs);
|
|
87
|
+
const date = new Date(ms);
|
|
88
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
89
|
+
const mm = String(date.getUTCMinutes()).padStart(2, "0");
|
|
90
|
+
return `${hh}:${mm}Z`;
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-color application for the embed's SVG mini-map (pure): the file's
|
|
3
|
+
* side-color layer paints markers per side — each party's matcher
|
|
4
|
+
* (nationality / org / tags) is evaluated against the entity dimensions the
|
|
5
|
+
* snapshot carries. The format's EntityResultItem declares only
|
|
6
|
+
* id/type/label/lat/lon, so TAG matchers apply to the item's stream `type`
|
|
7
|
+
* (the kinds≡tags convention the recorded behavior already uses for heat
|
|
8
|
+
* selections); nationality/org matchers evaluate the same-named fields when
|
|
9
|
+
* a richer snapshot carries them. Style variants: solid (base), stripe
|
|
10
|
+
* (alternate base/secondary by within-side index), accent (base fill +
|
|
11
|
+
* secondary outline).
|
|
12
|
+
*/
|
|
13
|
+
import type { GeovisionSideColorLayer } from "@zephytiju/lattice-common-bundle/geovisionView";
|
|
14
|
+
/** Entity dimensions the matchers see (item type + optional enrichment). */
|
|
15
|
+
export interface SideColorEntity {
|
|
16
|
+
readonly type: string;
|
|
17
|
+
readonly nationality?: string;
|
|
18
|
+
readonly org?: string;
|
|
19
|
+
readonly tags?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/** The resolved paint of one entity under a side-color layer. */
|
|
22
|
+
export interface SideColorPaint {
|
|
23
|
+
readonly sideId: string;
|
|
24
|
+
readonly baseToken: string;
|
|
25
|
+
readonly fillToken: string;
|
|
26
|
+
readonly outlineToken: string | null;
|
|
27
|
+
readonly style: "solid" | "stripe" | "accent";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the paint for one entity: the FIRST side whose parties match
|
|
31
|
+
* wins; unmatched entities keep the host palette (null). The within-side
|
|
32
|
+
* index drives stripe alternation across the file's entity order.
|
|
33
|
+
*/
|
|
34
|
+
export declare function paintFor(entity: SideColorEntity, layer: GeovisionSideColorLayer, withinSideIndex: number): SideColorPaint | null;
|
|
35
|
+
/** Within-side indices for the entity list (order of first-seen members). */
|
|
36
|
+
export declare function withinSideIndices(entities: readonly (SideColorEntity & {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
})[], layer: GeovisionSideColorLayer): ReadonlyMap<string, number>;
|
|
39
|
+
//# sourceMappingURL=sideColoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sideColoring.d.ts","sourceRoot":"","sources":["../../src/map/sideColoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gDAAgD,CAAC;AAE9F,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,iEAAiE;AACjE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;CAC/C;AAkBD;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,uBAAuB,EAC9B,eAAe,EAAE,MAAM,GACtB,cAAc,GAAG,IAAI,CAkBvB;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,CAAC,eAAe,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,EAChE,KAAK,EAAE,uBAAuB,GAC7B,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAc7B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-color application for the embed's SVG mini-map (pure): the file's
|
|
3
|
+
* side-color layer paints markers per side — each party's matcher
|
|
4
|
+
* (nationality / org / tags) is evaluated against the entity dimensions the
|
|
5
|
+
* snapshot carries. The format's EntityResultItem declares only
|
|
6
|
+
* id/type/label/lat/lon, so TAG matchers apply to the item's stream `type`
|
|
7
|
+
* (the kinds≡tags convention the recorded behavior already uses for heat
|
|
8
|
+
* selections); nationality/org matchers evaluate the same-named fields when
|
|
9
|
+
* a richer snapshot carries them. Style variants: solid (base), stripe
|
|
10
|
+
* (alternate base/secondary by within-side index), accent (base fill +
|
|
11
|
+
* secondary outline).
|
|
12
|
+
*/
|
|
13
|
+
function partyMatches(party, entity) {
|
|
14
|
+
const { nationality, org, tags } = party.match;
|
|
15
|
+
if (nationality !== undefined && entity.nationality === nationality)
|
|
16
|
+
return true;
|
|
17
|
+
if (org !== undefined && entity.org === org)
|
|
18
|
+
return true;
|
|
19
|
+
if (tags !== undefined && tags.length > 0) {
|
|
20
|
+
// Kinds≡tags: the item's stream type counts as a tag (see module header).
|
|
21
|
+
if (tags.includes(entity.type))
|
|
22
|
+
return true;
|
|
23
|
+
const entityTags = entity.tags ?? [];
|
|
24
|
+
if (tags.some((tag) => entityTags.includes(tag)))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the paint for one entity: the FIRST side whose parties match
|
|
31
|
+
* wins; unmatched entities keep the host palette (null). The within-side
|
|
32
|
+
* index drives stripe alternation across the file's entity order.
|
|
33
|
+
*/
|
|
34
|
+
export function paintFor(entity, layer, withinSideIndex) {
|
|
35
|
+
for (const side of layer.sides) {
|
|
36
|
+
const party = side.parties.find((candidate) => partyMatches(candidate, entity));
|
|
37
|
+
if (party === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const fillToken = party.style === "stripe" && withinSideIndex % 2 === 1
|
|
40
|
+
? party.secondaryToken ?? side.baseToken
|
|
41
|
+
: side.baseToken;
|
|
42
|
+
const outlineToken = party.style === "accent" ? party.secondaryToken ?? null : null;
|
|
43
|
+
return {
|
|
44
|
+
sideId: side.id,
|
|
45
|
+
baseToken: side.baseToken,
|
|
46
|
+
fillToken,
|
|
47
|
+
outlineToken,
|
|
48
|
+
style: party.style,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Within-side indices for the entity list (order of first-seen members). */
|
|
54
|
+
export function withinSideIndices(entities, layer) {
|
|
55
|
+
const counters = new Map();
|
|
56
|
+
const result = new Map();
|
|
57
|
+
for (const entity of entities) {
|
|
58
|
+
for (const side of layer.sides) {
|
|
59
|
+
const party = side.parties.find((candidate) => partyMatches(candidate, entity));
|
|
60
|
+
if (party === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
const count = counters.get(side.id) ?? 0;
|
|
63
|
+
counters.set(side.id, count + 1);
|
|
64
|
+
result.set(entity.id, count);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-token → CSS color resolution for the SVG mini-map: marker/blob
|
|
3
|
+
* colors resolve the HOST theme's Mantine tokens (--mantine-color-<token>-filled)
|
|
4
|
+
* through the embed container's computed style — the embed ships no palette
|
|
5
|
+
* of its own. Falls back to a small neutral ramp when a token is unresolvable
|
|
6
|
+
* (e.g. a bare test container), so the map stays legible everywhere.
|
|
7
|
+
*/
|
|
8
|
+
/** Reads the token's color from the container's computed style (no cache — theme swaps must apply). */
|
|
9
|
+
export declare function cssTokenColor(container: HTMLElement | null, token: string): string;
|
|
10
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/map/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-token → CSS color resolution for the SVG mini-map: marker/blob
|
|
3
|
+
* colors resolve the HOST theme's Mantine tokens (--mantine-color-<token>-filled)
|
|
4
|
+
* through the embed container's computed style — the embed ships no palette
|
|
5
|
+
* of its own. Falls back to a small neutral ramp when a token is unresolvable
|
|
6
|
+
* (e.g. a bare test container), so the map stays legible everywhere.
|
|
7
|
+
*/
|
|
8
|
+
const FALLBACKS = {
|
|
9
|
+
threat: "#ff6b6b",
|
|
10
|
+
warn: "#ffa94d",
|
|
11
|
+
accent: "#62B3FF",
|
|
12
|
+
signal: "#c0a8ff",
|
|
13
|
+
ok: "#69db7c",
|
|
14
|
+
marker: "#8fa3bf",
|
|
15
|
+
};
|
|
16
|
+
/** Reads the token's color from the container's computed style (no cache — theme swaps must apply). */
|
|
17
|
+
export function cssTokenColor(container, token) {
|
|
18
|
+
if (container !== null) {
|
|
19
|
+
const value = getComputedStyle(container).getPropertyValue(`--mantine-color-${token}-filled`);
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (trimmed.length > 0)
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
return FALLBACKS[token] ?? "#8fa3bf";
|
|
25
|
+
}
|