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',