Wind chevron overlay on the route map (cyclist-first UX)#39
Open
radumarias wants to merge 11 commits into
Open
Wind chevron overlay on the route map (cyclist-first UX)#39radumarias wants to merge 11 commits into
radumarias wants to merge 11 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
What you see
12kn) with tabular numerals.NOW · TUE 14:00scrubber 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 greenNOWbadge appears when the selected hour is the current one.Cyclist-first UX choices
backdrop-filter: blur(8px)on the pillPipeline
/api/route,/api/multi-routewindSamples: Array<{ point, headingDeg, hours[] }>— per-sample-point, per-hour{ time, windDirDeg, windKn, gustKn }.types.tsWindSampletype.wind-map.ts(new)chevronsForHour(),pickNowHour(),worstClass(). Pure helpers.MapViewwindChevronsprop. Separate$effectso scrubbing the hour re-renders only the chevrons — markers and polyline stay put.WindMapOverlay.svelte(new)RouteView,WaypointsViewi18n
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 test— 91/91 pass (3 new for chevronsForHour, worstClass).pnpm check— 0 errors.pnpm build— succeeds.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.Test plan
Out of scope (deferred)
title=tooltip, which is fine for desktop but not ideal on mobile. Easy follow-up.https://claude.ai/code/session_01B9DMrLPT7hYfdCTsJgFF9q
Generated by Claude Code