diff --git a/src/lib/features/timeline/TimeLine.svelte b/src/lib/features/timeline/TimeLine.svelte index 8f4018d..ba7cf9c 100644 --- a/src/lib/features/timeline/TimeLine.svelte +++ b/src/lib/features/timeline/TimeLine.svelte @@ -73,15 +73,29 @@
- +
+ + {#if hasData && $timelineStore.markers.length > 0} +
+ {#each $timelineStore.markers as m} + {@const pct = $timelineStore.max > 0 ? m.time / $timelineStore.max : 0} + + {fmtHms(m.time)} + + {/each} +
+ {/if} +
{fmtHms(elapsed)} {fmtHms(duration)} @@ -107,6 +121,60 @@ opacity: 0.7; } + .range-wrapper { + position: relative; + } + + .marker-ticks { + position: absolute; + inset: 0; + pointer-events: none; + } + + .marker-tick { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 3px; + height: 14px; + background: var(--tick-color, #dc3545); + border-radius: 1px; + pointer-events: auto; + cursor: default; + } + + .marker-tooltip { + display: none; + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background: var(--tick-color, #dc3545); + color: #fff; + font-size: 0.7rem; + font-family: monospace; + padding: 2px 7px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + z-index: 1010; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); + } + + .marker-tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: var(--tick-color, #dc3545); + } + + .marker-tick:hover .marker-tooltip { + display: block; + } + @media (max-width: 767.98px) { .timeline-container { min-width: calc(100vw - 24px); diff --git a/src/lib/features/timeline/index.ts b/src/lib/features/timeline/index.ts index d7ab97b..4cb5edd 100644 --- a/src/lib/features/timeline/index.ts +++ b/src/lib/features/timeline/index.ts @@ -1,3 +1,3 @@ export { timelineStore } from './store'; -export type { TimelineState } from './store'; +export type { TimelineState, TimelineMarker } from './store'; export { default as TimeLine } from './TimeLine.svelte'; diff --git a/src/lib/features/timeline/store.ts b/src/lib/features/timeline/store.ts index 50f08c3..6c3017f 100644 --- a/src/lib/features/timeline/store.ts +++ b/src/lib/features/timeline/store.ts @@ -11,12 +11,18 @@ import { writable } from 'svelte/store'; * locally, but the UI slider always operates over the global range. */ +export interface TimelineMarker { + time: number; + color: string; +} + export interface TimelineState { time: number; min: number; max: number; speed: number; playing: boolean; + markers: TimelineMarker[]; } const initial: TimelineState = { @@ -25,6 +31,7 @@ const initial: TimelineState = { max: 0, speed: 1, playing: false, + markers: [], }; function createTimeline() { @@ -87,7 +94,11 @@ function createTimeline() { }); } - return { subscribe: store.subscribe, play, pause, reset, seek, setSpeed, setRange }; + function setMarkers(markers: TimelineMarker[]) { + store.update((s) => ({ ...s, markers })); + } + + return { subscribe: store.subscribe, play, pause, reset, seek, setSpeed, setRange, setMarkers }; } export const timelineStore = createTimeline(); diff --git a/src/lib/features/workspaces/WorkspaceRenderer.svelte b/src/lib/features/workspaces/WorkspaceRenderer.svelte index 39a9d1c..961d7d7 100644 --- a/src/lib/features/workspaces/WorkspaceRenderer.svelte +++ b/src/lib/features/workspaces/WorkspaceRenderer.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/map/index.ts b/src/lib/map/index.ts index a57319a..80af90b 100644 --- a/src/lib/map/index.ts +++ b/src/lib/map/index.ts @@ -1,6 +1,6 @@ export * from './core'; export { createMapLibreMap } from './maplibre'; -export { plotPrediction, plotTelemetry, plotAnimatedMarker } from './layers'; +export { plotPrediction, plotTelemetry, plotAnimatedMarker, plotEndMarker } from './layers'; export type { TrajectoryStyle } from './layers'; export { startCoordinateSelection } from './tools/selection'; export { startMeasure } from './tools/measure'; diff --git a/src/lib/map/layers.ts b/src/lib/map/layers.ts index 2a03e31..4f79848 100644 --- a/src/lib/map/layers.ts +++ b/src/lib/map/layers.ts @@ -104,8 +104,17 @@ export function plotTelemetry( } } +export function plotEndMarker(scene: Scene, lng: number, lat: number): void { + scene.addCircle('marker-core', { + center: [lng, lat], + radiusPx: 7, + color: '#6c757d', + strokeColor: '#ffffff', + strokeWidth: 2, + }); +} + export function plotAnimatedMarker(scene: Scene, lng: number, lat: number): void { - scene.clear(); scene.addCircle('marker-ring', { center: [lng, lat], radiusPx: 14, diff --git a/src/lib/map/maplibre.ts b/src/lib/map/maplibre.ts index 32adfd5..5b85861 100644 --- a/src/lib/map/maplibre.ts +++ b/src/lib/map/maplibre.ts @@ -101,7 +101,15 @@ class MapLibreScene implements Scene { addCircle(id: string, options: CircleOptions): MapLayer { const layerId = this.scopeId(id); - if (this.map.getSource(layerId)) this.remove(id); + const existing = this.map.getSource(layerId) as maplibregl.GeoJSONSource | undefined; + if (existing) { + existing.setData({ + type: 'Feature', + properties: {}, + geometry: { type: 'Point', coordinates: options.center }, + }); + return { id: layerId, remove: () => this.remove(id) }; + } this.map.addSource(layerId, { type: 'geojson',