Skip to content
Closed
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
35 changes: 26 additions & 9 deletions weather-voodoo/src/lib/components/MapView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,35 @@
drawn.push(marker);
}

// Camera fits don't require the style to be loaded — run them eagerly so
// shared links open already framed on the route. Prefer polyline bounds
// (a routed sea/trail polyline can detour well outside the markers' bbox)
// and fall back to marker bounds when no route line is available yet.
const fitTarget = polyline && polyline.length >= 2 ? polyline : markers;
if (fitTarget.length >= 2) {
const bounds = new maplibregl.LngLatBounds();
fitTarget.forEach((p) => bounds.extend([p.lon, p.lat]));
map.fitBounds(bounds, { padding: 60, maxZoom: 11, duration: 400 });
} else if (markers.length === 1) {
map.flyTo({ center: [markers[0].lon, markers[0].lat], zoom: 11, duration: 400 });
}

// The route source/layer needs the MapLibre style to be loaded first; on a
// cold page load (shared link) this often isn't ready yet, so defer until
// the 'load' event if necessary.
if (map.isStyleLoaded()) {
renderRouteLayer();
} else {
map.once('load', renderRouteLayer);
}
}

function renderRouteLayer() {
if (!map) return;
if (map.getLayer('route-line')) map.removeLayer('route-line');
if (map.getSource('route-src')) map.removeSource('route-src');

if (polyline && polyline.length >= 2 && map.isStyleLoaded()) {
if (polyline && polyline.length >= 2) {
map.addSource('route-src', {
type: 'geojson',
data: {
Expand All @@ -172,14 +197,6 @@
paint: { 'line-color': polylineColor, 'line-width': 3, 'line-opacity': 0.85 }
});
}

if (markers.length >= 2 && map.isStyleLoaded()) {
const bounds = new maplibregl.LngLatBounds();
markers.forEach((m) => bounds.extend([m.lon, m.lat]));
map.fitBounds(bounds, { padding: 60, maxZoom: 11, duration: 400 });
} else if (markers.length === 1) {
map.flyTo({ center: [markers[0].lon, markers[0].lat], zoom: 11, duration: 400 });
}
}
</script>

Expand Down
10 changes: 8 additions & 2 deletions weather-voodoo/src/lib/components/RouteView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
type RouteMeta =
| { kind: 'ferry'; lengthKm: number; wayCount: number; originSnapKm: number; destinationSnapKm: number }
| { kind: 'sea'; lengthKm: number; greatCircleKm: number; detourRatio: number }
| { kind: 'straight'; ferryFallback?: string; ferryDetail?: string };
| { kind: 'trail'; lengthKm: number; wayCount: number; hikeWayCount: number; bikeWayCount: number; originSnapKm: number; destinationSnapKm: number }
| { kind: 'straight'; ferryFallback?: string; ferryDetail?: string; trailFallback?: string; trailDetail?: string };
let result = $state<{
hours: FusedHour[];
timezone: string;
Expand Down Expand Up @@ -168,9 +169,14 @@
{t('route.seaPrefix')} <strong>{result.route.lengthKm.toFixed(0)} km</strong>
<span title={t('route.detourTitle')}>(×{result.route.detourRatio.toFixed(2)} {t('route.greatCircleSuffix')})</span>
</div>
{:else if result?.route.kind === 'trail'}
<div class="muted route-meta">
{t('route.trailPrefix')} <strong>{result.route.lengthKm.toFixed(0)} km</strong>
<span title={t('route.trailWaysTitle')}>{t('route.trailWaysSuffix', { hike: result.route.hikeWayCount, bike: result.route.bikeWayCount })}</span>
</div>
{:else if view.from && view.to && result}
<div class="muted route-meta">
{t('route.straight')}{#if result.route.kind === 'straight' && result.route.ferryFallback} ({t('route.ferryLabel')}: {result.route.ferryFallback}{#if result.route.ferryDetail} · {result.route.ferryDetail}{/if}){/if} {t('route.straightHint')}
{t('route.straight')}{#if result.route.kind === 'straight' && result.route.ferryFallback} ({t('route.ferryLabel')}: {result.route.ferryFallback}{#if result.route.ferryDetail} · {result.route.ferryDetail}{/if}){/if}{#if result.route.kind === 'straight' && result.route.trailFallback} ({t('route.trailLabel')}: {result.route.trailFallback}{#if result.route.trailDetail} · {result.route.trailDetail}{/if}){/if} {t('route.straightHint')}
</div>
{/if}
</div>
Expand Down
2 changes: 2 additions & 0 deletions weather-voodoo/src/lib/components/WaypointsView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
legCount: number;
ferryLegs: number;
seaLegs: number;
trailLegs: number;
straightLegs: number;
totalKm: number;
};
Expand Down Expand Up @@ -282,6 +283,7 @@
· {t(result.route.legCount === 1 ? 'waypoints.legsOne' : 'waypoints.legsMany', { n: result.route.legCount })}
{#if result.route.ferryLegs > 0} · ⛴️ {t('waypoints.ferryLegs', { n: result.route.ferryLegs })}{/if}
{#if result.route.seaLegs > 0} · ⚓ {t('waypoints.seaLegs', { n: result.route.seaLegs })}{/if}
{#if result.route.trailLegs > 0} · 🥾 {t('waypoints.trailLegs', { n: result.route.trailLegs })}{/if}
{#if result.route.straightLegs > 0} · 📐 {t('waypoints.straightLegs', { n: result.route.straightLegs })}{/if}
</div>
{:else if editing && draft.length >= 2}
Expand Down
18 changes: 14 additions & 4 deletions weather-voodoo/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type Dict = {
legsMany: string;
ferryLegs: string;
seaLegs: string;
trailLegs: string;
straightLegs: string;
straightPreview: string;
editAriaLabel: string;
Expand All @@ -114,6 +115,10 @@ export type Dict = {
seaPrefix: string;
detourTitle: string;
greatCircleSuffix: string;
trailPrefix: string;
trailWaysSuffix: string;
trailWaysTitle: string;
trailLabel: string;
straight: string;
straightHint: string;
ferryLabel: string;
Expand Down Expand Up @@ -344,7 +349,7 @@ export const en: Dict = {
title: 'How Weather Voodoo works',
lede: 'It picks the best hours of the next 3 days for a specific outdoor or marine trip — by blending wind, gust, rain, wave height and visibility into a single 0–100 score, then surfacing the best contiguous windows that fit your time and duration.',
twoTabs: 'The tabs',
routeDesc: '<strong>Route</strong> — pick a <em>From</em> and <em>To</em>. The forecast is fused across 3 sample points along the line, taking the <em>worst-case</em> conditions hour-by-hour. Good for ferry/boat trips, drives, kayak crossings.',
routeDesc: '<strong>Route</strong> — pick a <em>From</em> and <em>To</em>. The forecast is fused across 3 sample points along the line, taking the <em>worst-case</em> conditions hour-by-hour. In <strong>Sea</strong> mode the route follows OSM ferry ways or the open-ocean network; in <strong>Land</strong> mode it follows OSM hiking and cycling trails.',
fixedDesc: '<strong>Fixed location</strong> — one place. Good for a beach day, hike, sunset session.',
score: 'The score',
scoreDesc: "Each hour is scored 0–100 based on the activity type. The window score is the <em>worst hour</em> in the range (chain-as-strong-as-weakest-link); average score is the tiebreaker.",
Expand Down Expand Up @@ -401,6 +406,7 @@ export const en: Dict = {
legsMany: '{n} legs',
ferryLegs: '{n} ferry',
seaLegs: '{n} open-ocean',
trailLegs: '{n} trail',
straightLegs: '{n} straight',
straightPreview: '📐 Straight-line preview while editing — press Done to compute the real route.',
editAriaLabel: 'Edit waypoint',
Expand All @@ -412,15 +418,19 @@ export const en: Dict = {
},
route: {
computing: 'Computing route…',
computingLong: 'Computing best sea route & fetching forecasts… first request in a region can take up to 15 s.',
computingLong: 'Computing best route & fetching forecasts… first request in a region can take up to 15 s.',
ferryPrefix: '⛴️ Ferry route via OpenStreetMap:',
waysSuffix: '({n} ways in the area)',
waysTitle: 'number of distinct ferry ways considered',
seaPrefix: '⚓ Open-ocean route:',
detourTitle: 'route length / great-circle length',
greatCircleSuffix: 'the great-circle line',
straight: "📐 Straight-line route — couldn't snap to a sea-route network.",
straightHint: 'Sample points may cross land.',
trailPrefix: '🥾 Trail route via OpenStreetMap:',
trailWaysSuffix: '({hike} hiking · {bike} cycling ways nearby)',
trailWaysTitle: 'paths, footways, tracks, bridleways and cycleways within the search area',
trailLabel: 'trail',
straight: "📐 Straight-line route — couldn't snap to a routing network.",
straightHint: 'Sample points may cross land or sea.',
ferryLabel: 'ferry'
},
days: {
Expand Down
13 changes: 9 additions & 4 deletions weather-voodoo/src/lib/i18n/ro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const ro: Dict = {
title: 'Cum funcționează Weather Voodoo',
lede: 'Aplicația alege cele mai bune ore din următoarele 3 zile pentru o anumită călătorie în natură sau pe mare — combinând vântul, rafala, ploaia, înălțimea valului și vizibilitatea într-un singur scor 0–100, apoi afișând cele mai bune intervale contigue care se potrivesc cu timpul și durata ta.',
twoTabs: 'Tab-urile',
routeDesc: '<strong>Rută</strong> — alege un <em>De la</em> și un <em>Până la</em>. Prognoza este fuzionată din 3 puncte de eșantionare de-a lungul traseului, luând condițiile <em>cele mai severe</em> oră de oră. Bun pentru feribot/barcă, condus, traversări cu caiacul.',
routeDesc: '<strong>Rută</strong> — alege un <em>De la</em> și un <em>Până la</em>. Prognoza este fuzionată din 3 puncte de eșantionare de-a lungul traseului, luând condițiile <em>cele mai severe</em> oră de oră. În modul <strong>Marin</strong> ruta urmează căile de feribot OSM sau rețeaua oceanică; în modul <strong>Terestru</strong> urmează potecile de drumeție și pistele de bicicletă din OSM.',
fixedDesc: '<strong>Locație fixă</strong> — un singur loc. Bun pentru zi la plajă, drumeție, sesiune de apus.',
score: 'Scorul',
scoreDesc: 'Fiecare oră primește scor 0–100 în funcție de tipul activității. Scorul intervalului este <em>cea mai slabă oră</em> din interval (lanțul e la fel de rezistent ca veriga cea mai slabă); scorul mediu este criteriu de departajare.',
Expand Down Expand Up @@ -105,6 +105,7 @@ export const ro: Dict = {
legsMany: '{n} segmente',
ferryLegs: '{n} feribot',
seaLegs: '{n} larg',
trailLegs: '{n} potecă',
straightLegs: '{n} drept',
straightPreview: '📐 Previzualizare în linie dreaptă în timpul editării — apasă Gata pentru a calcula ruta reală.',
editAriaLabel: 'Editează punctul de trecere',
Expand All @@ -116,15 +117,19 @@ export const ro: Dict = {
},
route: {
computing: 'Se calculează ruta…',
computingLong: 'Se calculează cea mai bună rută marină și se descarcă prognoza… prima cerere într-o regiune poate dura până la 15 s.',
computingLong: 'Se calculează cea mai bună rută și se descarcă prognoza… prima cerere într-o regiune poate dura până la 15 s.',
ferryPrefix: '⛴️ Rută cu feribot via OpenStreetMap:',
waysSuffix: '({n} căi în zonă)',
waysTitle: 'număr de căi de feribot distincte luate în calcul',
seaPrefix: '⚓ Rută în larg:',
detourTitle: 'lungimea rutei / lungimea great-circle',
greatCircleSuffix: 'din linia great-circle',
straight: '📐 Rută în linie dreaptă — nu am putut potrivi cu o rețea de rute marine.',
straightHint: 'Punctele de eșantionare pot traversa uscatul.',
trailPrefix: '🥾 Rută pe poteci via OpenStreetMap:',
trailWaysSuffix: '({hike} poteci de drumeție · {bike} piste de bicicletă în zonă)',
trailWaysTitle: 'poteci, trotuare, drumuri forestiere, alei călări și piste de bicicletă din zona de căutare',
trailLabel: 'potecă',
straight: '📐 Rută în linie dreaptă — nu am putut potrivi cu o rețea de rute.',
straightHint: 'Punctele de eșantionare pot traversa uscat sau mare.',
ferryLabel: 'feribot'
},
days: {
Expand Down
13 changes: 9 additions & 4 deletions weather-voodoo/src/lib/i18n/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const th: Dict = {
title: 'Weather Voodoo ทำงานอย่างไร',
lede: 'แอปจะเลือกชั่วโมงที่ดีที่สุดในอีก 3 วันข้างหน้าสำหรับทริปกลางแจ้งหรือทะเลที่เฉพาะเจาะจง — โดยรวมลม กระโชก ฝน ความสูงของคลื่น และทัศนวิสัยเป็นคะแนน 0–100 แล้วแสดงช่วงที่ดีที่สุดที่เข้ากับเวลาและระยะเวลาของคุณ',
twoTabs: 'แท็บต่าง ๆ',
routeDesc: '<strong>เส้นทาง</strong> — เลือก <em>จาก</em> และ <em>ถึง</em> พยากรณ์ถูกรวมจาก 3 จุดตัวอย่างตามเส้นทาง โดยใช้สภาพ<em>ที่แย่ที่สุด</em>ในแต่ละชั่วโมง เหมาะกับทริปเรือเฟอร์รี/เรือ ขับรถ ข้ามแม่น้ำด้วยคายัค',
routeDesc: '<strong>เส้นทาง</strong> — เลือก <em>จาก</em> และ <em>ถึง</em> พยากรณ์ถูกรวมจาก 3 จุดตัวอย่างตามเส้นทาง โดยใช้สภาพ<em>ที่แย่ที่สุด</em>ในแต่ละชั่วโมง โหมด <strong>ทะเล</strong> จะใช้เส้นทางเฟอร์รี OSM หรือเครือข่ายทะเลเปิด; โหมด <strong>บก</strong> จะใช้เส้นทางเดินป่าและจักรยานจาก OSM',
fixedDesc: '<strong>ตำแหน่งคงที่</strong> — สถานที่เดียว เหมาะกับวันชายหาด เดินป่า ดูพระอาทิตย์ตก',
score: 'คะแนน',
scoreDesc: 'แต่ละชั่วโมงถูกให้คะแนน 0–100 ตามประเภทกิจกรรม คะแนนช่วง = ชั่วโมง<em>ที่แย่ที่สุด</em>ในช่วงนั้น (โซ่แข็งแรงเท่าจุดที่อ่อนที่สุด) คะแนนเฉลี่ยเป็นตัวแบ่งกรณีเสมอ',
Expand Down Expand Up @@ -105,6 +105,7 @@ export const th: Dict = {
legsMany: '{n} ช่วง',
ferryLegs: '{n} เฟอร์รี',
seaLegs: '{n} ทะเลเปิด',
trailLegs: '{n} เส้นทางเดิน',
straightLegs: '{n} เส้นตรง',
straightPreview: '📐 เส้นตรงตัวอย่างขณะแก้ไข — กดเสร็จเพื่อคำนวณเส้นทางจริง',
editAriaLabel: 'แก้ไขจุดผ่าน',
Expand All @@ -116,15 +117,19 @@ export const th: Dict = {
},
route: {
computing: 'กำลังคำนวณเส้นทาง…',
computingLong: 'กำลังคำนวณเส้นทางทะเลที่ดีที่สุด & ดึงข้อมูลพยากรณ์… ครั้งแรกในพื้นที่อาจใช้เวลาถึง 15 วินาที',
computingLong: 'กำลังคำนวณเส้นทางที่ดีที่สุด & ดึงข้อมูลพยากรณ์… ครั้งแรกในพื้นที่อาจใช้เวลาถึง 15 วินาที',
ferryPrefix: '⛴️ เส้นทางเฟอร์รีจาก OpenStreetMap:',
waysSuffix: '({n} เส้นทางในพื้นที่)',
waysTitle: 'จำนวนเส้นทางเฟอร์รีที่นำมาพิจารณา',
seaPrefix: '⚓ เส้นทางทะเลเปิด:',
detourTitle: 'ความยาวเส้นทาง / ความยาว great-circle',
greatCircleSuffix: 'ของเส้น great-circle',
straight: '📐 เส้นทางตรง — ไม่สามารถ snap เข้ากับเครือข่ายเส้นทางทะเลได้',
straightHint: 'จุดสุ่มอาจข้ามแผ่นดิน',
trailPrefix: '🥾 เส้นทางเดินเท้า/ปั่นจักรยานจาก OpenStreetMap:',
trailWaysSuffix: '({hike} เส้นเดินเท้า · {bike} เส้นจักรยาน ในบริเวณใกล้เคียง)',
trailWaysTitle: 'ทางเดิน ทางเท้า ทางลูกรัง ทางม้า และทางจักรยานในพื้นที่ค้นหา',
trailLabel: 'เส้นทางเดิน',
straight: '📐 เส้นทางตรง — ไม่สามารถ snap เข้ากับเครือข่ายเส้นทางได้',
straightHint: 'จุดสุ่มอาจข้ามแผ่นดินหรือทะเล',
ferryLabel: 'เฟอร์รี'
},
days: {
Expand Down
Loading