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:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.77.1",
|
"version": "1.77.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import type { Attachment } from 'svelte/attachments';
|
import type { Attachment } from 'svelte/attachments';
|
||||||
import {
|
import {
|
||||||
builder,
|
builder,
|
||||||
|
mapView,
|
||||||
nextWaypointId,
|
nextWaypointId,
|
||||||
scheduleSave
|
scheduleSave
|
||||||
} from './builderStore.svelte';
|
} from './builderStore.svelte';
|
||||||
|
import { SAC_TRAIL_COLOR } from '$lib/data/sacColors';
|
||||||
// Single-point Swisstopo elevation lookups are intentionally NOT used —
|
// Single-point Swisstopo elevation lookups are intentionally NOT used —
|
||||||
// they returned 0 against WGS-84 inputs in practice, and image waypoints
|
// they returned 0 against WGS-84 inputs in practice, and image waypoints
|
||||||
// don't need per-point altitudes anyway. Waypoint altitudes flow from
|
// 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.
|
// Lines: per-pair so each can carry a segIdx for inline insertion.
|
||||||
// Snapped + linear segments share the same visual styling — there's
|
// Snapped + linear segments share the same visual styling — there's
|
||||||
// no need to call out the difference, the user picked the mode.
|
// no need to call out the difference, the user picked the mode.
|
||||||
const primary =
|
// SAC white-red-white red — matches /hikes overview + detail-page
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim() ||
|
// trail colour so the live preview reads as the final published track.
|
||||||
'#5e81ac';
|
const trackColor = SAC_TRAIL_COLOR.T2;
|
||||||
if (builder.routedSegments.length > 0) {
|
if (builder.routedSegments.length > 0) {
|
||||||
builder.routedSegments.forEach((seg, segIdx) => {
|
builder.routedSegments.forEach((seg, segIdx) => {
|
||||||
const latLngs = seg.map((p) => [p[1], p[0]] as [number, number]);
|
const latLngs = seg.map((p) => [p[1], p[0]] as [number, number]);
|
||||||
const poly = L.polyline(latLngs, {
|
const poly = L.polyline(latLngs, {
|
||||||
color: primary,
|
color: trackColor,
|
||||||
weight: 4,
|
weight: 4,
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
}).addTo(lineLayer);
|
}).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.
|
// React to store changes.
|
||||||
const stopRoot = $effect.root(() => {
|
const stopRoot = $effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -166,6 +185,17 @@
|
|||||||
builder.routedSegments.length;
|
builder.routedSegments.length;
|
||||||
render();
|
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.
|
// Click on blank map. In normal mode, append a new waypoint at the end.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
builder,
|
builder,
|
||||||
insertWaypointChronologically,
|
insertWaypointChronologically,
|
||||||
nextWaypointId,
|
nextWaypointId,
|
||||||
|
requestFitBounds,
|
||||||
scheduleSave,
|
scheduleSave,
|
||||||
type Waypoint
|
type Waypoint
|
||||||
} from './builderStore.svelte';
|
} from './builderStore.svelte';
|
||||||
@@ -135,15 +136,23 @@
|
|||||||
// by snap-to-route enrichment) is the only reliable elevation
|
// by snap-to-route enrichment) is the only reliable elevation
|
||||||
// source against WGS-84 inputs, and its single-point variant kept
|
// source against WGS-84 inputs, and its single-point variant kept
|
||||||
// returning 0 even with workaround attempts.
|
// returning 0 even with workaround attempts.
|
||||||
|
let placedAny = false;
|
||||||
for (const p of prepared) {
|
for (const p of prepared) {
|
||||||
if (!p.ok) continue;
|
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
|
// Cache the original file so the waypoint table can show a
|
||||||
// full-resolution preview this session (for both new + matched
|
// full-resolution preview this session (for both new + matched
|
||||||
// waypoints). Persistence to localStorage keeps only the small
|
// waypoints). Persistence to localStorage keeps only the small
|
||||||
// thumbnail.
|
// thumbnail.
|
||||||
setFullImage(p.id, p.file);
|
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) {
|
function onDrop(e: DragEvent) {
|
||||||
|
|||||||
@@ -107,6 +107,17 @@ function defaultState(): BuilderState {
|
|||||||
|
|
||||||
export const builder = $state<BuilderState>(loadDraft());
|
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;
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
export function scheduleSave(): void {
|
export function scheduleSave(): void {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user