Skip to content

feat: satnavscii — terminal satnav with perspective rendering#1

Merged
gnathoi merged 33 commits into
mainfrom
feat/satnavscii-initial-build
Apr 1, 2026
Merged

feat: satnavscii — terminal satnav with perspective rendering#1
gnathoi merged 33 commits into
mainfrom
feat/satnavscii-initial-build

Conversation

@gnathoi

@gnathoi gnathoi commented Apr 1, 2026

Copy link
Copy Markdown
Member

Summary

satnavscii: a terminal-based car navigation system built on mapscii's Braille rendering engine. Perspective-tilted maps, route navigation, GPS tracking, and turn-by-turn directions, all in Unicode Braille characters.

TypeScript Migration

  • Full JS → TS port of all 12 source files
  • Vitest replacing jest (53 tests)
  • Type declarations for all untyped npm packages

Perspective Rendering (Phase 1)

  • PerspectiveRenderer.ts — Mode 7 projection with Camera state
  • Camera.ts — position, heading, pitch, FOV, near-plane clipping
  • GPXReplay.ts — GPX track file parser and timed replay
  • Progressive tile rendering (cached tiles render immediately)
  • Keyboard controls: arrow steering, zoom, pitch adjustment

Route Navigation (Phase 2)

  • RoutingService.ts — OSRM API + Nominatim geocoding (rate-limited, cached)
  • Route overlay (cyan polyline, draws last, min 1 cell wide)
  • --route "origin;destination" and --animate flags

Offline Tiles (Phase 2.5)

  • TileDownloader.ts — bulk region download with progress and rate limiting
  • satnavscii download --region "Berlin" --zoom 10-16

GPS Tracking (Phase 3)

  • GPSInput.ts — gpsd integration with speed-based jump filter
  • Speed-dependent zoom (log2 curve, no discontinuities)
  • --gps flag

Navigation HUD + Minimap (Phase 4)

  • Speed, distance, ETA, turn-by-turn instructions
  • Corner minimap with route overview and position dot

Raspberry Pi Kiosk (Phase 4.5)

  • --kiosk flag (fullscreen, auto-GPS, no quit)
  • satnavscii.service systemd unit file

Infrastructure

  • Containerfile for podman/docker
  • GitHub Actions CI (Node 18/20/22 + container smoke test)
  • README rewrite with full CLI docs, paying homage to mapscii

Bug Fixes (from 2 adversarial review passes, 23 issues fixed)

  • Styler all/none filter logic was inverted
  • TileSource._getHTTP returned undefined when persistence disabled
  • TileSource cached broken tiles before load completed
  • TileSource.init() didn't await async MBTiles open
  • Canvas.line() had swapped width/color arguments
  • Config singleton mutation leaked non-config fields
  • Plus 17 more (GPS jump filter, timer leaks, error swallowing, etc.)

Test Coverage

53 tests across 8 test files covering:

  • Perspective math (projection, clipping, heading rotation)
  • Styler filter logic (all/none/any/==/>=/in/has)
  • GPX parsing (valid files, errors, replay playback)
  • GPS distance calculations (haversine)
  • Routing service (geocoding, route fetching, step formatting)
  • Tile downloader (bounds calculation, estimates, geocoding)
  • Utils (hex2rgb, ll2tile round-trip, normalize, population)

Pre-Landing Review

Two full adversarial review passes. 23 total issues found and fixed. 0 remaining.

Test plan

  • All vitest tests pass (53 tests, 8 files)
  • Tile server connectivity verified (mapscii.me returns 200)
  • Route animation smoke test (Brandenburg Gate → Alexanderplatz)
  • Headless mode initialization verified

🤖 Generated with Claude Code

gnathoi and others added 30 commits April 1, 2026 11:41
…E rewrite

Phase 3: gpsd integration with GPS jump filter and speed-dependent zoom.
Phase 4: Navigation HUD with speed, distance, ETA, turn-by-turn display
         and corner minimap showing route overview.
Phase 4.5: Raspberry Pi kiosk mode (--kiosk flag + systemd service).

Also:
- Containerfile for podman/docker deployment
- GitHub Actions CI replacing Travis CI (Node 18/20/22 + container build)
- README rewrite with full CLI docs, paying homage to mapscii
- Remove stale .eslintrc.js, .travis.yml, snap/snapcraft.yaml, main.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-fixed (14):
- Canvas.line() swapped width/color args
- Promise.reject() without reason in both renderers
- GPS jump filter time-based bypass (now uses speed-based threshold)
- GPX parser time misalignment + attribute order assumption
- TileSource cache size 16→64 for perspective renderer
- TileSource caches broken tiles on load failure (now cache-after-load)
- Zoom range validation for tile download (cap at 16)
- Tile fetch timeout + HTTP status check
- Speed-to-zoom discontinuity (now smooth log2 curve)
- Containerfile reproducible tsx install
- Compass heading negative normalization
- GPXReplay interval leak on double-start
- Animation timer leak on double-start

User-approved fixes (5):
- Config singleton mutation (only MapsciiConfig keys applied, createConfig added)
- TileDownloader Nominatim rate limiting
- Tile visibility cap centered on camera position
- HTTPS for tile server URL
- Config refactor (createConfig factory, no more global mutation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- _draw() catch-all now only swallows "renderer busy", logs real errors
- PerspectiveRenderer no longer overwrites shared TileSource styler
- TileSource.init() now async, awaits loadMBTiles (was fire-and-forget race)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parts list, step-by-step setup from OS flash through test drive,
offline tile download, GPS configuration, kiosk auto-start,
mounting tips, and operational advice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rolldown (vitest's bundler) uses node:util.styleText which was added in Node 20.
Updated engines field and CI matrix accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The minimap was only drawing the route polyline on a blank background.
Now it reads cached tiles at a lower zoom level and renders roads,
water, landuse etc. as a proper top-down overview. Also pre-fetches
minimap tiles alongside main tiles so data is available by next frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increased size from 20% to 30% width / 40% height
- Roads drawn as connected polylines instead of individual pixels
- Water/landuse drawn as fill point clouds
- Skip noisy layers (buildings, labels, POIs) — only water, land, roads, admin
- Proper coordinate transform with configurable view span
- Route overlay drawn 2px wide for visibility
- Position dot enlarged to 5x5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Water: dark blue, Landuse: dark green, Roads: light gray, Admin: dim gray
- Polygons (water, land) drawn as connected outlines, not scattered pixels
- Roads drawn as connected polylines with proper layer-specific colors
- Route: bright cyan 2px wide, Position: white 5x5 dot
- Render order: fills first, lines on top, route on top, position last
- Colors no longer inherited from tile styles (which were all white/gray)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fill features can have points as Point[] or Point[][] depending on
geometry type. Now detects the shape before iterating, and guards
against undefined/malformed point data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Minimap:
- Now uses the SAME tiles already loaded by the main perspective view
  (guaranteed available, no separate low-zoom fetches that may not load)
- Roads render as connected polylines using tile feature data at full zoom
- Water/landuse drawn as polygon outlines
- Dedicated color palette: blue water, green land, gray roads, cyan route
- Shows 3x wider area than the main view for good overview context

Vehicle arrow:
- Classic GPS triangle arrow on the main perspective view (bottom-center,
  always pointing up since the map rotates around the vehicle)
- Same arrow on the minimap, rotated to match camera heading
- Arrow drawn as filled triangle in Braille sub-pixels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…led arrow

Minimap:
- Now reads tile.data.layers (ALL features in each tile) instead of
  tile.layers (only features visible in the narrow perspective frustum).
  This is the root cause of "only seeing the route" — roads to the left,
  right, and behind the camera were being excluded.
- Roads in all directions now visible as gray polylines on dark background.

Vehicle arrow:
- Now drawn as a proper filled triangle using scanline fill, not just edges.
- Main view arrow: size 10 (was 6), bright cyan-green color (stands out).
- Minimap arrow: size 6 (was 4), white, rotated by heading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed minimap entirely (wasn't rendering roads properly)
- Removed nav HUD overlay (speed, ETA, turn-by-turn) — deferred to issue
- Vehicle arrow now projects from camera position so it stays on the road
  instead of being fixed at an arbitrary screen position
- Arrow is larger (size 10) and filled with scanline rendering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ns in top half

Arrow:
- Fixed at bottom-center of screen, always visible. The vehicle doesn't
  move in a driving view — the map moves around it. Previous approach
  of projecting the camera position hit the near-plane and returned null.

Directions:
- Turn-by-turn instructions now rendered large in the top half of the
  terminal (above the horizon line where it's just empty sky). Drawn
  twice offset by 4 braille rows for a bold/thick effect.
- Bright cyan-green color to stand out.

Status bar remains small at the bottom (coords, heading, speed, ETA).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hack of drawing text twice offset by 4px with actual 2x-wide
characters: each letter is written twice at 2px offset to fill a 4px-wide
cell instead of the normal 2px. Single line, no duplication, wider text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Arrow:
- Finds the closest route point to the camera in map space
- Projects that point to screen space and draws the arrow there
- Arrow stays on the route as the camera moves along it
- Falls back to bottom-center if no route or projection fails

Direction text:
- Fixed "AArrrriivveedd" bug caused by writing each character twice
- Now uses normal canvas.text() centered in the top area above the horizon
- Clean single rendering, no character duplication

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Arrow:
- Fixed at bottom-center of screen like a real Garmin GPS. The map
  scrolls and rotates around the vehicle. The vehicle never moves.
- Removed route-chasing logic that caused the arrow to jump around.

Directions:
- Filter out noise maneuvers: "new name", "depart", "straight continue"
  These produced "new straight name onto..." text that made no sense.
- Only show actionable turns: left, right, fork, roundabout, exit, arrive
- Human-readable formatting: "Turn left onto Friedrichstraße (200m)"
  instead of "turn left onto Friedrichstraße (200m)"
- Added isActionableStep() filter with tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mples

Directions:
- "(200m)" was the road segment length (useless). Now shows distance
  FROM your current position TO the upcoming turn: "In 200m, Turn left"
- Matches how every real GPS displays turn-by-turn info
- Range increased to 800m so turns show up earlier

Examples:
- Changed all example routes from Berlin to Jersey Channel Islands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y Hub

8.5km route across Jersey, 13 min drive. Both buildings geocode correctly
via Nominatim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was advancing 1 route point per 100ms regardless of distance between
points. A 220-point 8.5km route finished in 22 seconds (~1400 km/h).

Now advances by distance: 14 m/s (50 km/h) at 200ms ticks = 2.8m per
tick. The 8.5km Jersey route now takes ~10 minutes, matching the real
13-minute OSRM estimate. ETA countdown is now based on remaining
distance / speed instead of point-count progress.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sky flickering:
- Features were rendering above the horizon (screen.y between 0 and
  horizon). Now clips at horizon line, not y=0. Nothing draws in the
  sky area except the direction text.

Route gap:
- Route points near the camera were dropped by near-plane clipping,
  creating a gap between the route line and the vehicle arrow.
  Now clamps near-plane points to bottom-center of screen so the
  route always connects to the vehicle position.

Speed:
- Bumped from 50 km/h (too slow for a demo) to 120 km/h.
  The 8.5km Jersey route now takes ~4 minutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route overlay:
- Only draws from current position forward (like a real GPS).
  Previously drew the entire route including passed segments, which
  caused multiple lines converging on the vehicle at the start.
- Route line starts from the vehicle arrow position and extends ahead.
- Behind-camera points are skipped, not clamped to bottom-center.

Ctrl+C:
- SIGINT handler cleans up timers, GPS, mouse, restores terminal
  state (cursor visibility, colors, raw mode) before exiting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Raw mode on stdin captures Ctrl+C as a keypress event (key.ctrl +
key.name === 'c') instead of delivering SIGINT. Added handler in
_onKey to catch it and call _cleanup() for a clean terminal exit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recorded driving from Digital Jersey Exchange to Digital Jersey Hub.
Converted from screen recording to 800px 12fps GIF (320KB).
Added *.mov to .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recorded new demo at native resolution, converted to H.264 1080p
(550KB, web-optimized with faststart). Much sharper than the 800px GIF.
Embedded as HTML video tag with autoplay/loop/muted for GitHub.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Preserves original mapscii copyright holders, adds satnavscii
contributors line for 2026.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gnathoi and others added 3 commits April 1, 2026 16:02
…video tags

GitHub markdown ignores <video> tags. Replaced MP4 with a 1200px wide
GIF at 15fps quality 90 (736KB). Renders natively in README.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- All references updated from satnavscii to SatNavSCII
- GitHub URLs point to hest-hq/SatNavSCII
- Container registry ghcr.io/hest-hq/SatNavSCII
- LICENSE updated with Hest attribution
- Containerfile labels updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gnathoi gnathoi merged commit 5fbd945 into main Apr 1, 2026
3 checks passed
@gnathoi gnathoi deleted the feat/satnavscii-initial-build branch April 1, 2026 15:07
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.

1 participant