+
+
+
+
+
{#if pendingWaypoint}
Klicke auf die Karte, um das Bild zu platzieren.
@@ -387,6 +467,97 @@
cursor: crosshair;
}
+ /* Bottom-right round controls + layer popover — same language as the
+ * detail / overview maps. */
+ .map-controls {
+ position: absolute;
+ bottom: 1rem;
+ right: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ align-items: flex-end;
+ z-index: 500;
+ }
+
+ .round-btn {
+ display: grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ background: var(--color-surface);
+ color: var(--color-text-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 50%;
+ box-shadow: var(--shadow-md);
+ cursor: pointer;
+ transition:
+ color var(--transition-fast),
+ background var(--transition-fast),
+ transform var(--transition-fast),
+ box-shadow var(--transition-fast);
+ }
+
+ .round-btn:hover {
+ color: var(--color-primary);
+ transform: scale(1.05);
+ box-shadow: var(--shadow-hover);
+ }
+
+ .layer-menu {
+ position: relative;
+ }
+
+ .layer-popover {
+ position: absolute;
+ right: calc(100% + 0.5rem);
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ padding: 0.3rem;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ min-width: 9.5rem;
+ white-space: nowrap;
+ }
+
+ .layer-popover button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.45rem;
+ padding: 0.45rem 0.7rem;
+ border: 0;
+ background: transparent;
+ color: var(--color-text-primary);
+ font: inherit;
+ font-size: 0.85rem;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ text-align: left;
+ transition: background var(--transition-fast), color var(--transition-fast);
+ }
+
+ .layer-popover button :global(svg) {
+ color: var(--color-text-tertiary);
+ flex: 0 0 auto;
+ }
+
+ .layer-popover button:hover {
+ background: var(--color-bg-elevated);
+ }
+
+ .layer-popover button.active {
+ background: var(--color-primary);
+ color: var(--color-text-on-primary);
+ }
+
+ .layer-popover button.active :global(svg) {
+ color: var(--color-text-on-primary);
+ }
+
.placement-banner {
position: absolute;
top: 0.75rem;
diff --git a/src/lib/data/mapTiles.ts b/src/lib/data/mapTiles.ts
new file mode 100644
index 00000000..8d4569ca
--- /dev/null
+++ b/src/lib/data/mapTiles.ts
@@ -0,0 +1,30 @@
+/**
+ * Map tile sources for the hikes maps.
+ *
+ * Tiles are served through the region-switching caching proxy (see
+ * `tile-proxy/`), which transparently picks swisstopo inside Switzerland and
+ * global providers (OpenTopoMap / Esri) elsewhere. The app just uses one
+ * canonical scheme and never talks to the providers directly.
+ *
+ * To point back at swisstopo directly (e.g. local dev without the proxy),
+ * change `TILE_BASE` here — it's the single switch.
+ */
+export const TILE_BASE = 'https://maps.bocken.org';
+
+export const TILE_URL = {
+ /** Schematic / topographic ("Karte"). */
+ karte: `${TILE_BASE}/karte/{z}/{x}/{y}`,
+ /** Satellite ("Luftbild"). */
+ luftbild: `${TILE_BASE}/luftbild/{z}/{x}/{y}`,
+ /** Historical Dufour map — Switzerland only. */
+ dufour: `${TILE_BASE}/dufour/{z}/{x}/{y}`
+} as const;
+
+/** Combined attribution — the proxy may serve any provider depending on the
+ * region in view, so all three are credited. Shown in the page footer (the
+ * on-map control is disabled). */
+export const TILE_ATTRIBUTION =
+ '©
swisstopo · ' +
+ '©
OpenStreetMap, ' +
+ '
OpenTopoMap · ' +
+ '©
Esri';
diff --git a/src/lib/hikes/hikeArea.ts b/src/lib/hikes/hikeArea.ts
index bfcc15c1..ec3ae7ae 100644
--- a/src/lib/hikes/hikeArea.ts
+++ b/src/lib/hikes/hikeArea.ts
@@ -25,3 +25,15 @@ export function resolveHikeArea(
if (k) return { value: `country:${k.code}`, label: k.name, iconUrl: k.flagUrl, kind: 'country' };
return null;
}
+
+/** Whether a hike sits in a swisstopo-covered region (Switzerland or
+ * Liechtenstein). Drives the schematic max-zoom (swisstopo reaches deeper than
+ * the global fallback) and whether the CH/LI-only Dufour layer is offered. */
+export function isSwissRegion(
+ canton: string | null | undefined,
+ country: string | null | undefined
+): boolean {
+ if (resolveCanton(canton)) return true;
+ const c = resolveCountry(country);
+ return c?.code === 'CH' || c?.code === 'LI';
+}
diff --git a/src/routes/hikes/+page.svelte b/src/routes/hikes/+page.svelte
index 717f386e..f1cc9b94 100644
--- a/src/routes/hikes/+page.svelte
+++ b/src/routes/hikes/+page.svelte
@@ -253,9 +253,12 @@
credit to appear somewhere on the page. -->
diff --git a/src/routes/hikes/[slug]/+page.svelte b/src/routes/hikes/[slug]/+page.svelte
index 100904d1..2c43cbc9 100644
--- a/src/routes/hikes/[slug]/+page.svelte
+++ b/src/routes/hikes/[slug]/+page.svelte
@@ -4,6 +4,7 @@
import HikeStageNav from '$lib/components/hikes/HikeStageNav.svelte';
import ElevationProfile from '$lib/components/hikes/ElevationProfile.svelte';
import { stage, clearActiveStage } from '$lib/components/hikes/stageStore.svelte';
+ import { isSwissRegion } from '$lib/hikes/hikeArea';
import Seo from '$lib/components/Seo.svelte';
import { setHikeContext } from '$lib/components/hikes/hikeContext.svelte';
import { listScrollAnchors } from '$lib/components/hikes/scrollAnchors';
@@ -55,6 +56,9 @@
const canton = $derived(resolveCanton(hike.canton));
const trackColor = $derived(sacTrailColor(hike.difficulty));
+ // swisstopo covers CH + LI; abroad the schematic caps lower (OpenTopoMap z17)
+ // and the Dufour layer is unavailable.
+ const inSwissRegion = $derived(isSwissRegion(hike.canton, hike.country));
// Publish date formatted in long German for the meta footer
// (matches the hike's `date: YYYY-MM-DD` frontmatter format).
@@ -287,7 +291,7 @@
type="application/json"
crossorigin="anonymous"
/>
-
+
@@ -357,6 +361,7 @@
showPrivate
{trackColor}
{stages}
+ swissRegion={inSwissRegion}
initialCenter={heroPose?.center}
initialZoom={heroPose?.zoom}
onReady={() => (heroMapReady = true)}
@@ -476,7 +481,7 @@