Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 361 additions & 15 deletions weather-voodoo/src/lib/components/MapView.svelte

Large diffs are not rendered by default.

117 changes: 114 additions & 3 deletions weather-voodoo/src/lib/components/RouteView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import PlaceSearch from './PlaceSearch.svelte';
import PlacesChips from './PlacesChips.svelte';
import MapView from './MapView.svelte';
import WindMapOverlay from './WindMapOverlay.svelte';
import WindCompass from './WindCompass.svelte';
import DayTabs from './DayTabs.svelte';
import ForecastTable from './ForecastTable.svelte';
import TripFinder from './TripFinder.svelte';
import { filterHoursForDay, localIsoDate } from '$lib/time';
import { addRecent } from '$lib/client/recentPlaces.svelte';
import { t } from '$lib/i18n/index.svelte';
import type { DaylightDay, FusedHour, LabeledPoint, DayKey } from '$lib/types';
import { chevronsForHour, pickNowHour, worstClass } from '$lib/wind-map';
import type { RelativeWindClass } from '$lib/wind';
import type { DaylightDay, FusedHour, LabeledPoint, DayKey, WindSample } from '$lib/types';

let loading = $state(false);
let error = $state<string | null>(null);
Expand All @@ -23,8 +27,13 @@
daylight: DaylightDay[];
polyline: { lat: number; lon: number }[];
route: RouteMeta;
windSamples: WindSample[];
} | null>(null);

// Currently-selected hour for the on-map wind chevron overlay. null = auto
// (use the first hour >= now). Resets when a new route loads.
let mapHour = $state<string | null>(null);

const markers = $derived(
[view.from, view.to].filter((m): m is LabeledPoint => m !== null)
);
Expand Down Expand Up @@ -58,6 +67,7 @@
daylight?: DaylightDay[];
polyline?: { lat: number; lon: number }[];
route?: RouteMeta;
windSamples?: WindSample[];
};
result = {
hours: data.hours,
Expand All @@ -67,8 +77,11 @@
{ lat: from.lat, lon: from.lon },
{ lat: to.lat, lon: to.lon }
],
route: data.route ?? { kind: 'straight' }
route: data.route ?? { kind: 'straight' },
windSamples: data.windSamples ?? []
};
// Reset scrubber to "now" whenever a new route comes in.
mapHour = null;
})
.catch((e: unknown) => {
if (e instanceof DOMException && e.name === 'AbortError') return;
Expand All @@ -87,6 +100,34 @@
);
const activeMode = $derived(eff.mode);

// Wind-overlay scrubber state.
const hourTimes = $derived(result?.windSamples?.[0]?.hours.map((h) => h.time) ?? []);
const nowTime = $derived(hourTimes.length > 0 ? pickNowHour(hourTimes) : null);
const selectedTime = $derived(mapHour ?? nowTime ?? hourTimes[0] ?? '');
const classLabelMap = $derived<Record<RelativeWindClass, string>>({
head: t('wind.head'),
'head-cross': t('wind.headCross'),
cross: t('wind.cross'),
'tail-cross': t('wind.tailCross'),
tail: t('wind.tail')
});
const chevrons = $derived(
result && selectedTime
? chevronsForHour(result.windSamples, selectedTime, (c) => classLabelMap[c])
: []
);
const verdictKeyMap = $derived<Record<RelativeWindClass, string>>({
head: 'windMap.verdict.head',
'head-cross': 'windMap.verdict.headCross',
cross: 'windMap.verdict.cross',
'tail-cross': 'windMap.verdict.tailCross',
tail: 'windMap.verdict.tail'
});
const verdict = $derived.by(() => {
const w = worstClass(chevrons);
return w ? t(verdictKeyMap[w]) : undefined;
});

let lastFocused: 'from' | 'to' | null = $state(null);

function pickFrom(p: LabeledPoint) {
Expand Down Expand Up @@ -119,6 +160,14 @@
view.day = d;
}

let compassVisible = $state(
typeof localStorage !== 'undefined' ? localStorage.getItem('wx-compass') !== 'hidden' : true
);
function setCompassVisible(v: boolean) {
compassVisible = v;
if (typeof localStorage !== 'undefined') localStorage.setItem('wx-compass', v ? 'visible' : 'hidden');
}

let fullscreen = $state(false);
function toggleFullscreen() {
fullscreen = !fullscreen;
Expand Down Expand Up @@ -176,7 +225,44 @@
</div>

<div class="card map-card" class:loading-overlay={loading} style="padding: 0;">
<MapView {markers} {polyline} onPick={onMapPick} height={fullscreen ? '100%' : undefined} />
<MapView
{markers}
{polyline}
onPick={onMapPick}
height={fullscreen ? '100%' : undefined}
showUserLocation={chevrons.length > 0}
/>
{#if chevrons.length > 0 && hourTimes.length > 0 && selectedTime}
<WindMapOverlay
{hourTimes}
{selectedTime}
{nowTime}
timezone={result?.timezone}
{verdict}
onSelect={(t) => (mapHour = t)}
/>
{/if}
{#if chevrons.length > 0}
<div class="wind-compass-anchor">
{#if compassVisible}
<WindCompass
relWindDeg={chevrons[0].relWindDeg}
windKn={chevrons[0].windKn}
cls={chevrons[0].cls}
classLabel={chevrons[0].classLabel}
onHide={() => setCompassVisible(false)}
/>
{:else}
<button
type="button"
class="wc-show-btn"
onclick={() => setCompassVisible(true)}
title={t('windMap.showCompass')}
aria-label={t('windMap.showCompass')}
>🧭</button>
{/if}
</div>
{/if}
{#if loading && view.from && view.to}
<div class="map-loading" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
Expand Down Expand Up @@ -258,6 +344,31 @@
.map-card {
position: relative;
}
.wc-show-btn {
all: unset;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
background: rgba(15, 23, 42, 0.88);
border: 1.5px solid rgba(148, 163, 184, 0.25);
border-radius: 50%;
cursor: pointer;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
}
.wc-show-btn:hover {
background: rgba(15, 23, 42, 0.95);
border-color: rgba(255, 255, 255, 0.3);
}
.wind-compass-anchor {
position: absolute;
bottom: 14px;
left: 14px;
z-index: 12;
pointer-events: auto;
}
.map-loading {
position: absolute;
top: 0.6rem;
Expand Down
Loading