feat(route-builder): SAC-red trail + refit map on image drop

Trail polyline now uses the same SAC white-red-white red as the /hikes
overview and detail pages so the live preview reads as the final
published track. Dropping geolocated images now reframes the edit map
to the new bounds via a shared `mapView.fitTick` signal — covers GPX
imports too if they ever wire it up.
This commit is contained in:
2026-05-19 21:22:34 +02:00
parent 3b524e9c70
commit 7bede8cd64
4 changed files with 56 additions and 6 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.77.1",
"version": "1.77.2",
"private": true,
"type": "module",
"scripts": {
@@ -2,9 +2,11 @@
import type { Attachment } from 'svelte/attachments';
import {
builder,
mapView,
nextWaypointId,
scheduleSave
} from './builderStore.svelte';
import { SAC_TRAIL_COLOR } from '$lib/data/sacColors';
// Single-point Swisstopo elevation lookups are intentionally NOT used —
// they returned 0 against WGS-84 inputs in practice, and image waypoints
// don't need per-point altitudes anyway. Waypoint altitudes flow from
@@ -135,14 +137,14 @@
// Lines: per-pair so each can carry a segIdx for inline insertion.
// Snapped + linear segments share the same visual styling — there's
// no need to call out the difference, the user picked the mode.
const primary =
getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim() ||
'#5e81ac';
// SAC white-red-white red — matches /hikes overview + detail-page
// trail colour so the live preview reads as the final published track.
const trackColor = SAC_TRAIL_COLOR.T2;
if (builder.routedSegments.length > 0) {
builder.routedSegments.forEach((seg, segIdx) => {
const latLngs = seg.map((p) => [p[1], p[0]] as [number, number]);
const poly = L.polyline(latLngs, {
color: primary,
color: trackColor,
weight: 4,
opacity: 0.9
}).addTo(lineLayer);
@@ -155,6 +157,23 @@
}
}
function fitToTrack() {
const points: [number, number][] = [];
for (const w of builder.waypoints) {
if (w.unplaced) continue;
points.push([w.lat, w.lng]);
}
for (const seg of builder.routedSegments) {
for (const p of seg) points.push([p[1], p[0]]);
}
if (points.length === 0) return;
if (points.length === 1) {
map.setView(points[0], 13);
return;
}
map.fitBounds(L.latLngBounds(points), { padding: [40, 40] });
}
// React to store changes.
const stopRoot = $effect.root(() => {
$effect(() => {
@@ -166,6 +185,17 @@
builder.routedSegments.length;
render();
});
// External fit-bounds requests (image drops, GPX imports).
// The map's own init-time auto-fit covers first-load; this
// effect handles every subsequent batch insertion.
let lastTick = mapView.fitTick;
$effect(() => {
const tick = mapView.fitTick;
if (tick === lastTick) return;
lastTick = tick;
fitToTrack();
});
});
// Click on blank map. In normal mode, append a new waypoint at the end.
@@ -3,6 +3,7 @@
builder,
insertWaypointChronologically,
nextWaypointId,
requestFitBounds,
scheduleSave,
type Waypoint
} from './builderStore.svelte';
@@ -135,15 +136,23 @@
// by snap-to-route enrichment) is the only reliable elevation
// source against WGS-84 inputs, and its single-point variant kept
// returning 0 even with workaround attempts.
let placedAny = false;
for (const p of prepared) {
if (!p.ok) continue;
if (p.kind === 'new') insertWaypointChronologically(p.wp);
if (p.kind === 'new') {
insertWaypointChronologically(p.wp);
if (p.hasGps) placedAny = true;
}
// Cache the original file so the waypoint table can show a
// full-resolution preview this session (for both new + matched
// waypoints). Persistence to localStorage keeps only the small
// thumbnail.
setFullImage(p.id, p.file);
}
// Reframe the map to the new track. Only matters when the batch
// added at least one geolocated waypoint — unplaced images don't
// affect bounds, and matched-only drops leave coords unchanged.
if (placedAny) requestFitBounds();
}
function onDrop(e: DragEvent) {
@@ -107,6 +107,17 @@ function defaultState(): BuilderState {
export const builder = $state<BuilderState>(loadDraft());
/**
* UI-only signal for the edit map: bumping `fitTick` asks the map to
* re-run `fitBounds()` on the current track. Used after batch insertions
* (image drops, GPX import) where the user expects the map to reframe to
* show every newly-added waypoint. Not persisted.
*/
export const mapView = $state({ fitTick: 0 });
export function requestFitBounds(): void {
mapView.fitTick++;
}
let saveTimer: ReturnType<typeof setTimeout> | null = null;
export function scheduleSave(): void {
if (!browser) return;