Skip to content

feat(routes): shortest water routes between nurseries precomputed GeoJSON for move lines and animation (#183) - #187

Open
Luen wants to merge 15 commits into
feat/185-map-circle-keyboardfrom
feat/183-water-routes
Open

Luen wants to merge 15 commits into
feat/185-map-circle-keyboardfrom
feat/183-water-routes

Conversation

@Luen

@Luen Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #183.

Stacked on top of #186 (feat/185-map-circle-keyboard).

Summary of Changes

  1. Shortest Water Route Engine (src/lib/server/oceanRoutes.ts):

    • Adapted coastal water routing principles from hikeandexplore (pull 1661: water-route.mjs and paddle-route.mjs) for Moorea's coastline geometry (src/lib/moorea-outline.json, OpenStreetMap, ODbL 1.0).
    • Generates a 35 m raster grid with a 2,000 m buffer around Moorea island.
    • Computes distance-to-land via a two-pass chamfer distance transform, snapping nursery points to the nearest navigable coastal water.
    • Searches shortest water routes using grid A* with a clearance penalty near land to avoid scraping jagged headlands and reefs.
    • Smooths paths using raycast supercover string-pulling, reducing node counts while strictly guaranteeing zero land crossings.
    • Precomputes all 91 nursery pairs in ~700 ms, with 0 routes intersecting land.
  2. Database Cache & Importer:

    • Added nursery_routes table to Drizzle schema (drizzle/0006_keen_shaman.sql) storing (fromId, toId, km, straightKm, coordinates).
    • Bumped IMPORTER_VERSION = '12' in src/lib/server/import/index.ts so the disposable SQLite cache rebuilds and precomputes water routes on import.
    • Loaded routes into src/lib/server/data.ts with helper nurseryRoute(d, fromId, toId) (automatically reversing coordinates when fromId > toId).
  3. Multi-Point Polyline Geometry & Maps:

    • Extended src/lib/components/lineGeometry.ts:
      • Updated offsetPolyline to smoothly offset multi-point polylines rightwards for bidirectional travel separation (falling back to directedPoints for 2-point lines).
      • Added pointAlongPath for smooth distance-based interpolation along multi-point polylines.
    • Updated MooreaMap.svelte (SVG fallback) to render multi-point SVG paths along water routes with arrow markers.
    • Updated InteractiveMap.svelte (Leaflet) to render water route polylines with rightward lateral offset and marker-mid arrows.
    • Updated traveller animation on shark pages to glide smoothly along the water route, scaled by water distance (or jumping under prefers-reduced-motion).
  4. Pages & Copy:

    • /recaptures:
      • Move lines now trace water routes around Moorea.
      • Longest moves table displays both km (water) and km (direct).
      • Map caption and table note clarify that lines represent shortest water routes (estimates around Moorea's coast, not actual paths swum) and credit OpenStreetMap (ODbL 1.0).
    • /sharks/[id]:
      • Move slider and map notes display ≥ X km by water alongside direct straight-line distance.
      • Animated traveller dot navigates the water polyline.
  5. Tooling & Tests:

    • Added scripts/ocean-routes.mjs CLI tool and "routes": "node scripts/ocean-routes.mjs" script in package.json to verify routes and export GeoJSON (--geojson).
    • Added unit tests in src/lib/server/oceanRoutes.test.ts and src/lib/components/lineGeometry.test.ts.
    • Added Playwright e2e tests in e2e/map-routes.spec.ts testing routed move lines on /recaptures and traveller animation on /sharks/[id].
    • Updated AGENTS.md with water route documentation.

Verification

  • pnpm lint: Pass (0 errors)
  • pnpm check: Pass (0 errors, 0 warnings)
  • pnpm test: Pass (22 node tests + 136 vitest tests passing)
  • pnpm routes: Pass (91 pairs computed, 0 land crossings)
  • pnpm build: Pass
  • pnpm test:e2e: Pass (48 Playwright tests passing)

@Luen
Luen added this pull request to stack #188 November 12, 2026 01:27
@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Reviewed PR #187 changes across shortest water path precomputation (oceanRoutes.ts), GeoJSON water route dataset (data/moorea-water-routes.json), polyline and arrow rendering in InteractiveMap.svelte and MooreaMap.svelte, traveler playback animation on /sharks/[id], and distance displays on /recaptures.

Findings & Fixes Applied

  1. Coastline boundary rasterization & land-crossing checks:
    • buildMooreaWaterGrid() previously rasterized land only using scanlines through the Moorea perimeter ring. Coastline cells whose center (x + 0.5, y + 0.5) fell slightly outside the boundary were treated as open water even if a coastline segment crossed through the cell, causing 23 of 91 precomputed nursery routes to clip headlands or reef corners.
    • Added polygon edge traversal via traverse() to explicitly mark all boundary-intersecting cells as land.
    • In crossesLand(), added exact polygon line-segment intersection tests against outline.ring for every non-landing segment (including 2-point straight lines) and validated segment endpoints and midpoints against the raster grid.
    • Recomputed all routes (pnpm routes): 91/91 pairs now successfully follow open water with 0 land crossings.
    • Rounded route coordinates in data/moorea-water-routes.json to 5 decimal places (~1.1 m precision), keeping output compact without sacrificing path precision.
  2. Single directional arrowhead on water polylines:
    • In both MooreaMap.svelte and InteractiveMap.svelte, applying marker-mid="url(#ps-arrow-…)" to multi-segment water polylines caused an arrowhead to render on every interior waypoint vertex (up to 37 arrowheads on long routes such as Paopao → Haapiti).
    • Replaced multi-vertex marker application with a dedicated 3-point carrier line (polylineArrow()) placed at 60% of cumulative route distance, rendering exactly one clean directional arrowhead.
    • Widened polylineArrow() vertex separation from 0.1 px to 2 px so Leaflet integer pixel layer projection does not collapse flanking vertices.
    • Added noClip: true to the .ps-arrow polyline in InteractiveMap.svelte to prevent viewport clipping from dropping visible arrowheads.
  3. Distance-scaled traveler animation duration:
    • Move playback duration was previously fixed at 1200 ms regardless of whether a move was 1 km or 25 km, causing long routes to race and short hops to drag.
    • Introduced moveDuration(km) scaling smoothly from 750 ms up to 2500 ms based on route distance. Integrated into InteractiveMap.svelte traveller animation and /sharks/[id]/+page.svelte step playback.
  4. Distance reporting & schema/importer cache invalidation:
  5. Parent branch alignment & test suite:
    • Merged the updated feat/185-map-circle-keyboard (abc363e) into feat/183-water-routes.
    • Updated end-to-end browser tests (e2e/map-routes.spec.ts and e2e/map-keyboard.spec.ts) to verify water routed paths, distance badges, zoom durability, and playback animation.

Validation

  • pnpm lint: clean (0 errors)
  • pnpm check: clean (0 errors, 0 warnings)
  • pnpm routes: 91 pairs verified, 0 land crossings
  • pnpm test: 137 unit tests passed
  • Opt-in real data import test (REAL_DATA_DIR="data" pnpm vitest run src/lib/server/import/realData.test.ts): passed (13 seasons, 1310 sets, 3133 captures, 91 nursery routes)
  • pnpm build: successful production build
  • pnpm test:e2e: all 48 tests passed (including e2e/map-routes.spec.ts, e2e/map-keyboard.spec.ts, and light/dark axe checks)

Head commit: da8c358. Pushed to origin/feat/183-water-routes.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Update: Progressive Move Line Drawing along Water Route

Added progressive move line stroke drawing during playback and capture stepping on individual shark pages (/sharks/[id]), so lines smoothly draw themselves along the water route rather than jumping instantly into place between capture locations.

  1. Progressive Stroke Animation:
    • In InteractiveMap.svelte, the active move line matching the current playback step (traveller.lineId) receives dynamic strokeDasharray and strokeDashoffset keyed to the path's total length (path.getTotalLength()).
    • As step(now) advances over moveDuration(km), strokeDashoffset transitions from totalLen to 0, smoothly drawing the line forward along the Moorea coastline.
    • The directional arrowhead marker (.ps-arrow) starts hidden (opacity: 0) and fades in when the drawing line reaches the 60% mark (ARROW_AT = 0.6).
    • The traveller dot rides at the tip of the drawing line (path.getPointAtLength(totalLen * eased)), locking directly to the shifted polyline without deviation.
    • Upon completion (or when cancelled early/redrawn), dasharray and offset styles reset cleanly to default static strokes.
    • Preserves prefers-reduced-motion: reduce: lines and points render statically without animation.
  2. End-to-End Test:
    • Added browser test in e2e/map-routes.spec.ts verifying that the move line animates with active stroke dash properties during playback and resets cleanly upon completion.

Validation: pnpm check (0 errors), pnpm lint (0 errors), pnpm test (137 passed), pnpm build, pnpm test:e2e (all 49 passed).
Commit: 6608835.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed commit \c0ea012\ with follow-up polish:

  1. Autozoom & map bounds inclusion:

    • \InteractiveMap.svelte\ now includes all polyline vertices from \lineLayers\ when calculating \L.latLngBounds(all)\ (with \padding: [36, 36]), ensuring wide-curving offshore water routes (such as the southern route between Pointe de Paorea and Haapiti) are completely framed without clipping at the map borders.
  2. **Deduplication of map text on /recaptures**:

    • Trimmed the card \subtitle\ to concisely cover symbols, line thickness, and directionality, leaving the background methodology, OSM attribution, and conflict tally cleanly in the footnote note below the map.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed commit \58de034\ with bird's-eye view shark swim animation:

  1. Bird's-Eye View Shark Icon:

    • Replaced generic circle marker with an SVG dorsal/bird's-eye view silhouette of a shark (snout, flared pectoral fins, dorsal crest, pelvic fins, and oscillating caudal fin), styled to match the shark's species (e.g. blue body with black fin tips for blacktip reef sharks, warm gold for sicklefin lemons).
    • The shark rotates continuously to match the tangent heading of the water route as it rounds points and reef passes, while its body and tail gently undulate in an authentic swimming motion (\prefers-reduced-motion\ respected).
  2. Progressive Water Route Drawing:

    • As the shark swims forward from point A to point B over time, the water route line draws out directly behind its tail, with the directional arrow appearing at 60% of the path.
    • Paced smoothly across the water distance, remaining resting at the arrival nursery when finished.
  3. Journey Framing & Replay Controls:

    • Initial bounds calculation on shark pages now incorporates the entire journey's water routes (\�ounds\ prop), ensuring the full route and all turns stay within view regardless of initial playback step.
    • Initial state starts at Capture 1 with the shark visible and ready at Point A; clicking Play immediately starts the swim to Point B; upon completion the button switches to Replay for easy re-watching.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Update: Continuous Water Route Scrubbing & Smooth Swim Animation (Commit 3152c36)

  • Continuous Scrubbing with In-Betweens: Replaced discrete integer capture steps on the slider with fine-grained float progress (step="0.002"), enabling fluid scrubbing to any position between nurseries with real-time route tracing and distance feedback (e.g. Swimming: Pointe de Paorea → Haapiti · 7.9 of 15.8 km by water (50%)).
  • Smooth Playback Animation: Replaced the static timeout step jump with a continuous requestAnimationFrame playback loop that glides the slider thumb and animates the bird's-eye view shark smoothly from start to destination over time with responsive tail wagging and dynamic heading rotation along the curves of Moorea's coastline.
  • Quick-Jump Capture Navigation: Added accessible capture stop pills (Capture 1 · Pointe de Paorea, Capture 2 · Haapiti, and All moves) for instant 1-click jumps between specific stops.
  • Marker & Stroke Performance: Updated InteractiveMap.svelte to retain the Leaflet traveller marker instance across animation frames, mutating its position and SVG stroke dashoffset directly at 60–120 fps for zero-flicker movement.

All checks passed:

  • pnpm check: Pass (0 errors, 0 warnings)
  • pnpm lint: Pass
  • pnpm test: Pass (137 tests passing)
  • pnpm test:e2e: Pass (49 Playwright tests passing)

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes synchronized shark animation and continuous timeline scrubbing:

  1. Decoupled Timeline Scrubbing from Reduced Motion:

    • prefers-reduced-motion: reduce was previously setting still = true, forcing fraction = 1.0 and pinning the shark marker at the destination during manual slider dragging or playback.
    • Now, fraction and path positioning are driven directly by the slider timeline across all system settings, while prefers-reduced-motion respectfully disables rapid oscillating waggle/wiggle animations.
  2. Synchronous 1:1 Slider & Marker Lockstep:

    • Switched <input id="capture-slider"> to two-way bind:value={progress} to ensure native pointer/mouse scrubbing syncs synchronously with Svelte 5 runes state.
    • Derived moveLayers to avoid instantiating new layer arrays per render frame, eliminating layer rebuild thrash.
  3. Continuous Route Track (.ps-line-track):

    • Rendered an underlying subtle dashed guide track (.ps-line-track) along the water route so the entire path is visible, with the solid stroke filling in progressively up to the swimming shark marker.
    • Added robust DOM element fallback for SVG path length and heading calculation.

All 137 unit tests and 49 Playwright e2e tests pass.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Replaced the shark SVG marker with a clean, crisp circular traveller dot (L.circleMarker) styled with the species color and a crisp white border with drop shadow (.ps-traveller). The dot glides smoothly along the water route in lockstep with the continuous slider and progressive route line drawing.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Updated the underlying water route track (.ps-line-track) from dark navy/black (var(--ink)) to a clearly visible blue dotted line (var(--ps-line-0, #2563eb)) with increased opacity, matching the route stroke and standing out clearly against the ocean satellite imagery.

@Luen

Luen commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in commit bd023cc:

  • Directional arrow visibility on shark pages:
    • The directional arrow remains visible on the blue dotted track when the page first loads (at Capture 1 / starting point) so users can see the intended journey path.
    • As soon as Play is clicked or the slider is moved/scrubbed, the arrow is removed (\opacity: 0), leaving a clean solid water route line and travelling dot during animation and upon arrival.
    • Jumps back to Capture 1 cleanly restore the arrow on the initial track.
    • Multi-line static maps (such as /recaptures) preserve their directional arrows unchanged.

@Luen

Luen commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator Author

Updated in commit ebfbba2:

  • Default to 'All moves' with directional arrows on page load:
    • Shark detail pages now open in the All moves view by default whenever a shark has recorded moves between nurseries.
    • All water route lines are drawn with directional arrows visible on every leg (e.g. for sharks with multi-leg journeys like Apaura ↔ Vaiane, moves in both directions show their respective arrows).
    • The Play button shows Play on initial load.
    • Clicking Play starts the progressive leg-by-leg animation from Capture 1 with the traveller dot gliding along the water route (with arrows hidden during active playback to keep the solid animated lines clean).
    • When playback finishes at the final capture, the button displays Replay; clicking All moves returns to the complete overview with all arrows restored.

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.

Charts: shortest water routes between nurseries (precomputed GeoJSON) for move lines and animation

1 participant