Skip to content

Wind chevron overlay on the route map (cyclist-first UX)#39

Open
radumarias wants to merge 11 commits into
claude/relative-wind-directionfrom
claude/wind-map-overlay
Open

Wind chevron overlay on the route map (cyclist-first UX)#39
radumarias wants to merge 11 commits into
claude/relative-wind-directionfrom
claude/wind-map-overlay

Conversation

@radumarias
Copy link
Copy Markdown
Member

Summary

Builds on #38. Puts the relative-wind read-out where you actually need it while riding: directly on the map, glanceable at arm's length from a handlebar-mounted phone.

⚠️ This PR is stacked on #38 (base: claude/relative-wind-direction). When #38 merges, this PR's base will auto-rebase to main.

What you see

  • Big (48px) chevrons at each route sample point. The colored ring (red head → orange head-cross → grey cross → lime tail-cross → green tail) carries the meaning; the white arrow inside shows where the wind is going in absolute terms (so the geometry stays intuitive on the map). Dark backdrop + drop-shadow stays readable on any map background — bright sun, water, forest.
  • Bold wind speed label below each chevron (12kn) with tabular numerals.
  • NOW · TUE 14:00 scrubber pill at the top center of the map. ◀ ▶ buttons (48px tap target) advance one hour; the time label is also a button — tap to jump back to "now". A green NOW badge appears when the selected hour is the current one.
  • One-line verdict under the pill summarizing the worst-case class along the route — "Head wind along the route — tough going", "Tail wind — free speed", etc.

Cyclist-first UX choices

Design Reasoning
48px chevron ring, 4px colored border, white arrow inside Glanceable in 1-2 sec at arm's length on a handlebar mount. Color is the message, no reading required.
Dark backdrop + drop-shadow on chevrons + bold white speed label Survives bright sun, sweat on screen, light/dark map tiles.
Color band + arrow direction both convey Color-blind safe — even without color the arrow rotation tells you absolute wind direction.
48×48px touch targets on the scrubber Big enough for wet/cold/gloved fingers.
NOW button + pill is auto-default Opens on "right now" — the relevant hour for a moving rider. Scrub forward to plan ahead.
backdrop-filter: blur(8px) on the pill Keeps the map visible behind UI, looks like a HUD overlay.
Verdict line below the pill Lets the rider make a go/wait/turn-back decision without reading chevrons.

Pipeline

Layer Change
/api/route, /api/multi-route Return windSamples: Array<{ point, headingDeg, hours[] }> — per-sample-point, per-hour { time, windDirDeg, windKn, gustKn }.
types.ts New WindSample type.
wind-map.ts (new) chevronsForHour(), pickNowHour(), worstClass(). Pure helpers.
MapView New windChevrons prop. Separate $effect so scrubbing the hour re-renders only the chevrons — markers and polyline stay put.
WindMapOverlay.svelte (new) The scrubber pill + verdict line, absolutely positioned over the map.
RouteView, WaypointsView Wire up; compute the current-hour chevron set; hide chevrons while a Waypoints track is being edited (polyline isn't real yet).

i18n

New windMap.{regionLabel, now, nowTitle, jumpNowTitle, prevHour, nextHour, verdict.{head, headCross, cross, tailCross, tail}} keys in en / th / ro. Verdict copy is rider/sailor-friendly in each language ("free speed", "ฟรีแรง", "viteză gratuită").

Verified

  • pnpm test91/91 pass (3 new for chevronsForHour, worstClass).
  • pnpm check — 0 errors.
  • pnpm build — succeeds.
  • Preview API confirms per-sample-point per-hour wind: GET /api/route?from=7.74,98.78&to=8.05,98.81&samples=3&days=1&land=1 → 3 samples, each with 24h of wind data and a heading.
  • Preview deploy: https://weather-voodoo-3fdmlzh9s-xorio-s-projects.vercel.app

Test plan

  • Open the preview, pick a Route. Three big colored chevrons should appear ON the polyline at the sample points.
  • Check the colors match the head/tail badge in the table for the current hour.
  • Tap ◀ ▶ — chevrons should rotate and recolor smoothly as the hour changes; the time label updates.
  • Tap the time label — should snap back to "NOW" (green badge appears).
  • Verify on mobile (or narrow browser): chevrons remain readable at 42px, scrubber pill shrinks but stays tappable; timezone label hides under 720px.
  • Switch to fullscreen map — overlay stays positioned correctly.
  • Hover a chevron — title tooltip shows class, speed, absolute wind direction, heading, and relative angle.
  • Switch language to Thai / Romanian — verdict line and tooltip translate.
  • Waypoints view: chevrons appear when track is committed (not while editing).
  • Fixed location view: no chevrons or scrubber (no heading exists).

Out of scope (deferred)

  • Per-chevron tap → detail popup with hourly mini-table. Now uses a title= tooltip, which is fine for desktop but not ideal on mobile. Easy follow-up.
  • ETA-aware chevrons — show wind at the time the rider will be at that sample point (needs a speed assumption). Real value-add for long rides; would need a speed picker.
  • Trend strip — small horizontal "next 6 hours" colored bar above/below the scrubber. Nice but optional.
  • Auto-tick — pill auto-advances as wall clock advances. Won't matter for short trips, useful for multi-hour ones.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q


Generated by Claude Code

claude added 11 commits May 24, 2026 09:38
Builds on #38. Puts the relative-wind read-out where you actually need
it while riding/sailing: directly on the route map, glanceable at
arm's length from a handlebar-mounted phone.

What you see:
  * Big (48 px) chevrons at each route sample point. Colored ring
    (red head → orange head-cross → grey cross → lime tail-cross →
    green tail) carries the meaning; white arrow inside shows where
    the wind is going in absolute terms (so the geometry stays
    intuitive on the map). Dark backdrop + drop-shadow keeps them
    readable on any tile background — bright sun, water, forest.
  * Bold wind speed label below each chevron ("12kn") with tabular
    numerals.
  * "NOW · TUE 14:00" scrubber pill at the top of the map. ◀ ▶
    buttons (48 px tap target) advance one hour at a time; the label
    is also a button — tap it to jump back to "now". A green "NOW"
    badge appears when the selected hour is the current one.
  * One-line verdict under the pill summarizing the worst-case class
    along the route ("Head wind along the route — tough going",
    "Tail wind — free speed", …).

Wiring:
  * `/api/route` and `/api/multi-route` now return `windSamples` —
    per-sample-point, per-hour `{ time, windDirDeg, windKn, gustKn }`.
  * `WindSample` type added to types.ts.
  * `wind-map.ts` (new): `chevronsForHour()` builds the per-current-
    hour data shape MapView consumes; `pickNowHour()` picks the first
    forecast hour ≥ now; `worstClass()` feeds the scrubber verdict.
  * `MapView` gains a `windChevrons` prop and a separate render
    effect — chevrons re-render on hour scrub without touching
    markers or the polyline.
  * `WindMapOverlay.svelte` (new) — the scrubber pill + verdict
    line, absolutely positioned over the map. Big touch targets,
    backdrop-blur, hidden timezone label on narrow screens.
  * `RouteView` + `WaypointsView` wire it up; the chevron set is
    derived from the selected hour and re-renders reactively.
    Hidden while a Waypoints track is being edited (the polyline
    isn't real yet).

i18n: new `windMap.{regionLabel, now, nowTitle, jumpNowTitle,
prevHour, nextHour, verdict.*}` keys added to en / th / ro.

Tests: 3 new for `chevronsForHour` and `worstClass`. 91/91 pass.

Refs #37.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Live SVG compass widget in the bottom-left of the map showing the
relative-wind classification zones as colored arcs (red head → green
tail), a rotating white arrow pointing where the wind goes relative
to heading, wind speed in the center, and the class label below.

Zones that match the current wind class glow at full opacity; the
others are dimmed to 35% — so the "active" zone pops out. Arrow
smoothly transitions on hour scrub (CSS transition on the rotate).

Positioned absolutely inside the map-card, survives fullscreen.
Shows the first sample point's wind (where the cyclist currently is)
for the selected hour.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
When wind chevrons are visible (route is loaded), MapView requests
browser geolocation via watchPosition(). A pulsing blue dot (Strava /
Google Maps style) tracks the rider's position in real-time — the
pulse animation conveys "live" even at a glance.

Blue dot uses a MapLibre HTML Marker (not a layer) so it draws on top
of tiles & the polyline, and only requests location when a route is
active (no prompt on first page load).

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
The long paragraph of instructions took half the mobile screen. Now
the editing header shows a single-line hint ("Tap the map to add
waypoints. Press ✓ Done when ready.") plus a small ? button. Tapping
the ? opens a modal dialog with the full instructions.

The committed-track state still shows its short inline message since
it was already one line.

New i18n keys: waypoints.editHelpShort, waypoints.editHelpTitle in
en / th / ro.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Compass redesign:
  * Larger viewBox (140×140, was 120). Speed text is 22px (was 16)
    inside an inner exclusion zone (radius 22px). The arrow shaft
    starts at radius 26px, well outside the text — the number is
    always fully visible regardless of arrow orientation.
  * Colored ring is thicker (11px) for visibility.
  * Heading triangle bigger. Overall more readable at a glance.

Remove per-sample-point chevrons from the map — the compass +
scrubber + table badges are sufficient. The three floating circles
were confusing (users asked what they are), cluttered the map on
multi-waypoint routes, and showed data already visible in the compass
and the forecast table's wind column.

The `windChevrons` prop and rendering code in MapView stays (no dead
code removal needed in this commit — the prop is still typed and
renderable if a future view wants it), but neither RouteView nor
WaypointsView passes it now.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Compass redesign for maximum glance-ability:
  * ViewBox 150×150 (was 140). Ring is 16px thick (was 11) so the
    colored zones are much more visible. Arrow shaft is 5px wide
    (was 3.5) with a bigger arrowhead (16px, was 12). Heading
    triangle at top also larger.
  * Speed shows ONLY the number ("12" not "12kn") at 28px font
    (was 22). Unit is in the tap-detail panel, not the face.
  * Mobile touch: tapping the compass opens a bottom-sheet detail
    panel showing class, wind speed + unit, and relative angle.
    This replaces the title= tooltip (which doesn't work on mobile
    without long-press — bad UX for a cyclist with gloves). Tap
    again or tap backdrop to dismiss.

New i18n key: windMap.relAngle in en / th / ro.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Compass visual overhaul:
  * ViewBox 160×160, ring 18px thick. Active zone gets an SVG
    feDropShadow glow; inactive zones dimmed to 0.15 opacity (was
    0.3) so the contrast is much higher.
  * Arrow shaft 5.5px, arrowhead 18px wide, both get a colored glow
    matching the active class (feDropShadow filter). Very visible
    against the dark background.
  * Inner circle has a subtle colored fill (8% opacity of class
    color) reinforcing which zone is active.
  * Zone labels on the ring: H (head), × (cross), T (tail) — quick
    orientation for first-time viewers.
  * Class label below the compass now has a colored border matching
    the zone color.
  * Speed is 30px font, number only — the biggest element in center.

Hide / show toggle:
  * Small × button at top-right of the compass to hide it.
  * When hidden, a 🧭 button appears in the same anchor spot. Tap
    to bring the compass back.
  * State persisted in localStorage ('wx-compass': 'visible'|'hidden')
    so it survives page reloads.

New i18n keys: windMap.hideCompass, windMap.showCompass in en/th/ro.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Default map height bumped from 440px to 600px — gives more room for
the compass, scrubber, and polyline on desktop. On screens ≤ 720px
the map is capped at 55vh so it doesn't push the forecast table
off-screen.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Locate-me button:
  * Crosshair icon button (bottom-right of the map, above the OSM
    attribution). Tap → getCurrentPosition → flyTo with zoom ≥ 12.
    Button pulses blue while the GPS fix is in progress.
  * Uses the same blue-dot GPS marker that already tracks position.

Shared viewport:
  * New `mapViewport` reactive store (map-viewport.svelte.ts) holds
    `{ center, zoom }` across the session. Not URL-serialized — it's
    ephemeral view state.
  * MapView reads from the shared store on mount (so switching from
    Route → Waypoints keeps the same position). Falls back to the
    markers or the default view if no viewport is stored yet.
  * MapView writes to the shared store on every `moveend` event
    (any pan, zoom, flyTo, or fitBounds). So any move in one tab is
    visible when you switch to another.
  * fitBounds now only fires when the route data actually changes
    (fingerprinted by marker lat/lon), not on every $effect re-render.
    This prevents overriding the user's manual pan/zoom or the shared
    viewport.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
While editing waypoints, every added/moved point changed the marker
array which triggered fitBounds → zoomed out to show all markers at
maxZoom 11. This was disorienting when the user was zoomed in to place
precise points.

Fix: MapView gains a `suppressAutoFit` prop. WaypointsView passes
`true` while `editing` is active. The map stays at whatever zoom/pan
the user set — adding a point just draws the marker without moving
the camera. fitBounds still fires when:
  * A new route loads from the API (Route or Waypoints after Done)
  * Opening a shared link with markers in the URL
  * Switching tabs when no shared viewport exists yet

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
The most practical wind types (TAIL = go! and HEAD = stop!) should
be the most visually prominent. Previously TAIL-CROSS (lime #a3e635)
was brighter than TAIL (green #22c55e), and HEAD-CROSS (orange
#fb923c) was brighter than HEAD (red #f87171).

New color hierarchy — extremes pop, intermediates recede:
  HEAD:       #ff5555 (bright alarm red)
  HEAD-CROSS: #b45309 (muted amber)
  CROSS:      #64748b (quiet slate)
  TAIL-CROSS: #65a30d (muted olive)
  TAIL:       #4ade80 (bright mint green)

Glancing at the compass: bright red = bad, bright green = great,
everything else is in between. Applied consistently across compass,
table badges, and chevron CSS.

https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants