A modern, real-time streaming overlay for IRL streams with GPS tracking, weather display, heart rate monitoring, altitude/speed tracking, and smooth animated value transitions.
- 📍 Real-time GPS Location - Smart minimap that shows/hides based on movement
- 🌤️ Live Weather - Temperature with day/night-aware weather icons and conditions
- 💓 Heart Rate Monitor - Pulsoid integration with smooth animated value transitions
- 📊 Altitude & Speed - Real-time elevation and movement speed with smart auto-display
- 🌊 At-Sea Mode - Automatic water body detection for cruise/ocean streaming
- 🗺️ Smart Location Display - City, state, or custom location names with country flags
- ✨ Smooth Animations - Optimized value transitions for speed, altitude, and heart rate
- 🎨 Clean UI - Modern, responsive design optimized for OBS
npm installCreate .env.local in project root:
# Required - GPS Tracking
NEXT_PUBLIC_RTIRL_PULL_KEY=your_rtirl_pull_key
# Required - Location & Weather
NEXT_PUBLIC_LOCATIONIQ_KEY=your_locationiq_key
NEXT_PUBLIC_OPENWEATHERMAP_KEY=your_openweathermap_key
# Optional - Additional Features
NEXT_PUBLIC_PULSOID_TOKEN=your_pulsoid_token
# Note: Map tiles use free CartoDB (no API key needed)
# Optional - Admin Panel & Settings Sync
ADMIN_PASSWORD=your_admin_password
KV_REST_API_URL=your_vercel_kv_url
KV_REST_API_TOKEN=your_vercel_kv_token
KV_REST_API_READ_ONLY_TOKEN=your_vercel_kv_readonly_tokennpm run dev- Admin Panel:
http://localhost:3000 - Overlay:
http://localhost:3000/overlay
- Auto-show when moving (>5 km/h) - Shows when speed exceeds walking pace
- Auto-hide when stationary - Hides when speed drops below 5 km/h
- Day/night map styling - Based on real sunrise/sunset times from OpenWeatherMap API
- Fade transitions - Smooth 1-second fade in/out for better visual experience
The overlay supports 6 location display modes, each showing different levels of geographic detail:
-
Neighbourhood - Most precise: Shows neighbourhood/suburb/district names (e.g., "Downtown", "SoHo", "Shinjuku")
- Falls back to city → state → country if neighbourhood data unavailable
- Best for: City exploration, walking tours, detailed location tracking
-
City - City-level precision: Shows city/town/municipality names (e.g., "Austin", "Tokyo", "Paris")
- Falls back to state → country if city data unavailable
- Best for: General travel, transit, city-to-city movement
-
State - State/province level: Shows state/province/prefecture names (e.g., "California", "Tokyo Prefecture", "Ontario")
- Falls back to country if state data unavailable
- Best for: Regional tracking, state-level movement
-
Country - Broadest GPS mode: Shows only country name (e.g., "United States", "Japan", "France")
- Primary line stays empty, country shown on second line with flag
- Best for: International flights, country-level tracking
-
Custom - Manual text entry: Set your own location text (e.g., "Las Vegas Strip", "Tokyo Station")
- Optionally show country name and flag below custom text (toggle in admin panel)
- Works even without GPS data (useful for pre-planned locations)
- Best for: Specific landmarks, custom location names, pre-planned routes
-
Hidden - No location displayed: Completely hides location section
- Weather and minimap still function independently
- Best for: Weather-only or minimap-only overlays
The location display system uses a hierarchical fallback system with duplicate name detection to ensure clean, non-redundant location displays.
The system follows a strict hierarchy: neighbourhood → city → state → country
Each precision level checks all fields within its category before falling back to the next broadest category:
-
Neighbourhood Mode (
'neighbourhood'):- Checks:
neighbourhood,quarter,ward,borough,district,suburb - If no valid neighbourhood found → falls back to city
- If no city found → falls back to state
- If no state found → falls back to country
- Checks:
-
City Mode (
'city'):- Checks:
city,municipality,town,village,hamlet - If no valid city found → falls back to state
- If no state found → falls back to country
- Important: Never falls back to neighbourhood (would be more specific, not broader)
- Checks:
-
State Mode (
'state'):- Checks:
state,province,region,county - If no valid state found → falls back to country
- Important: Never falls back to city or neighbourhood (would be more specific)
- Checks:
-
Country Mode (
'country'):- Shows only country name (shortened if >20 characters)
- No fallback needed (country is the broadest category)
To prevent redundant displays like "Downtown Los Angeles, Los Angeles" or "Tokyo, Tokyo Prefecture", the system checks for overlapping names:
- Primary Line: Determined by selected precision level (neighbourhood/city/state/country)
- Second Line: Shows the next broadest category, but skips any names that overlap with the primary
Overlap Detection Rules:
- Checks if one name contains the other (case-insensitive)
- Checks if all words from shorter name appear in longer name
- Example: "Downtown Los Angeles" overlaps with "Los Angeles" → skip city, show state instead
- Example: "Tokyo" overlaps with "Tokyo Prefecture" → skip state, show country instead
Second Line Logic:
- For neighbourhood primary → tries city → state → country (skipping overlaps)
- For city primary → tries state → country (skipping overlaps)
- For state primary → tries country only (skipping overlaps)
- For country primary → no second line (country is broadest)
Location data from LocationIQ API is organized into categories (ordered from most appropriate to least appropriate):
- Neighbourhood Fields:
neighbourhood→quarter→ward→suburb→district→borough - City Fields:
city→municipality→town→county→village→hamlet - State Fields:
state→province→region - Country:
country(withcountryCodefor flag display)
Important Notes:
suburbis treated as a neighbourhood field, not a city fieldcountyis part of the city category (represents metropolitan areas like Gold Coast)- Fields are tried in order - first valid field found is used
- Field names vary by country but hierarchy is generally consistent worldwide
Before using a location name, it must pass validation:
- Not empty
- Not longer than 20 characters (display limit)
- Not just a number (e.g., "123", "5")
- Not ending with space + number (e.g., "Honmachi 3", "Block 12")
- Numbers in the middle are OK (e.g., "4th Avenue", "21st Street")
When switching display modes in the admin panel:
- Instant Update: Location re-formats immediately using cached location data (
lastRawLocation.current) - No GPS Wait: No need to wait for new GPS update - uses existing location data
- Fallback Respect: If selected mode has no data, falls back according to hierarchy
- Duplicate Prevention: Second line automatically adjusts to avoid duplicates
Example Scenarios:
- Switch from "Neighbourhood" to "City" → immediately shows city if available, or state if city unavailable
- Switch from "City" to "State" → immediately shows state if available, or country if state unavailable
- If location has "Downtown Los Angeles" (neighbourhood) and "Los Angeles" (city):
- Neighbourhood mode: "Downtown Los Angeles" (primary), "California" (state, city skipped due to overlap)
- City mode: "Los Angeles" (primary), "California" (state)
When GPS coordinates can't be reverse geocoded (ocean/remote areas):
- Automatically detects water bodies: "Gulf of Mexico 🇺🇸"
- Covers major seas and oceans worldwide
- Shows appropriate regional flag
- Temperature in both °C and °F
- Day/night weather icons (based on astronomical sunrise/sunset)
- Weather condition text (auto-shows for notable conditions like rain, snow, storms)
- Auto-updates every 5 minutes
- Location-based weather for your current GPS position
-
Altitude: Shows elevation in meters and feet
- Auto mode: Shows when elevation is above 500m (notable elevation like mountains/hills)
- Hides when elevation drops below 500m
- Hides when GPS data is stale (>1 minute old)
- Smooth animated transitions between values
- Auto mode: Shows when elevation is above 500m (notable elevation like mountains/hills)
-
Speed: Shows movement speed in km/h and mph
- Auto mode: Displays when speed ≥10 km/h and GPS data is fresh (<10 seconds old)
- Hides when GPS data is stale (>10 seconds old) - regardless of speed value
- Hides when speed <10 km/h (even if GPS is fresh)
- Smooth animated transitions between values
Access at http://localhost:3000 to configure:
-
Location Display - Choose precision level (Neighbourhood/City/State/Country) or custom text
- Changes apply immediately to overlay via real-time sync
- Location re-formats instantly when mode changes (uses cached location data)
- Each mode follows fallback hierarchy: neighbourhood → city → state → country
- Duplicate names automatically detected and skipped on second line
-
Custom Location - Enter custom text when "Custom" mode is selected
- Auto-saves after 1 second of no typing (debounced)
- Optional country name/flag display toggle
-
Weather - Show/hide temperature display
- Temperature updates every 5 minutes automatically
- Shows both °C and °F
-
Weather Condition - Control weather icon and description display:
- Always - Always show icon and description
- Auto - Show only for notable conditions (rain, snow, storms, fog, etc.)
- Hidden - Hide icon and description (temperature still shown)
-
Altitude Display - Control elevation display:
- Always - Always show altitude when GPS data available (even if stale)
- Auto - Shows when elevation is above 500m (notable elevation like mountains/hills)
- Hides when elevation drops below 500m
- Hides when GPS data is stale (>1 minute old)
- Hidden - Hide altitude completely
-
Speed Display - Control movement speed display:
- Always - Always show speed when GPS data available (even if stale)
- Auto - Show when speed ≥10 km/h AND GPS is fresh (<10 seconds old). Hides when GPS is stale (regardless of speed) or when speed <10 km/h
- Hidden - Hide speed completely
-
Minimap - Three display modes:
- Always Show - Minimap always visible (if GPS data available)
- Auto on Movement - Shows when speed >5 km/h, hides when stationary
- Hidden - Minimap completely hidden
-
Map Zoom - 6 levels from Continental (1) to Neighbourhood (13)
- Continental (1) - Trans-oceanic view
- Ocean (3) - Coastal view from sea
- Country (5) - Country view (matches "Country" location display mode)
- State (8) - State/province view (matches "State" location display mode)
- City (11) - Whole city view (matches "City" location display mode)
- Neighbourhood (13) - Streets & buildings (matches "Neighbourhood" location display mode)
-
To-Do List - Add, edit, complete, and delete tasks
- Shows on overlay when enabled
- Tasks sorted: incomplete first, then completed
Settings changes propagate to overlay in real-time via:
- Server-Sent Events (SSE) - Primary method, instant updates (<1 second)
- Public read-only access (no authentication required)
- Requires Vercel KV for persistence
- Falls back to polling if SSE unavailable
- Polling Fallback - Checks for updates every 2 seconds
- Used when SSE not available (e.g., no KV configured)
- Ensures settings eventually sync even without SSE
Security Model
- Read Access: Public (overlay needs to read settings)
/api/get-settings- GET only, read-only/api/settings-stream- GET only (SSE), read-only
- Write Access: Authenticated only (admin panel)
/api/save-settings- POST only, requires authentication- All other
/api/routes require authentication
Important: SSE messages include metadata (type, timestamp) that must be stripped before setting state. Always extract only settings properties when handling SSE updates.
- RealtimeIRL - GPS tracking (realtimeirl.com)
- LocationIQ - Reverse geocoding (locationiq.com)
- English Names: API requests include
accept-language=enparameter to request English location names - Normalization: Location names are normalized to English equivalents when possible
- Non-Latin Script Filtering: Location names with non-Latin alphabets (Japanese, Chinese, Arabic, Cyrillic, etc.) are automatically skipped, falling back to the next precision level (e.g., city → state → country)
- Accented Characters: Accented Latin characters (é, ñ, ü, etc.) are allowed and displayed normally
- Fallback: If English names aren't available or contain non-Latin scripts, the system automatically falls back to broader location levels
- English Names: API requests include
- OpenWeatherMap - Weather & sunrise/sunset (openweathermap.org)
- Pulsoid - Heart rate monitoring (pulsoid.net)
- Mapbox - Map tiles (mapbox.com)
- Vercel KV - Settings persistence (vercel.com/storage/kv)
vercel --prodSet environment variables in Vercel dashboard → Project Settings → Environment Variables
- Add Browser Source in OBS
- URL:
https://your-domain.com/overlay(orhttp://localhost:3000/overlayfor local development)- Note: The version parameter (
?v=...) is automatically added server-side - you don't need to include it manually
- Note: The version parameter (
- Width:
1920, Height:1080 - Check "Shutdown source when not visible"
- Refresh browser when scene becomes active: ✓ (Recommended for best performance)
Automatic Cache-Busting
The overlay automatically prevents caching issues:
- ✅ Server-Side Version Injection: Middleware adds
?v=<timestamp>to overlay URLs before OBS caches them - ✅ HTTP Cache Headers:
Cache-Control: no-cache, no-store, must-revalidateprevents browser caching - ✅ API Cache Busting: Settings API calls include timestamp parameters for fresh data
Settings Update Flow
- Admin panel saves settings → Settings stored in KV database
- Server-Sent Events (SSE) broadcasts update → All connected overlays receive update instantly
- Polling fallback → Overlays check for updates every 2 seconds if SSE unavailable
- Settings appear in OBS → No manual refresh needed!
If settings changes don't appear in OBS:
- Wait 2-5 seconds - Settings sync automatically via SSE/polling
- Refresh the browser source: Right-click → Refresh (if needed)
- Check browser console: Right-click browser source → Interact → F12 → Console tab
- Verify URL: Should include version parameter (e.g.,
/overlay?v=1234567890)
Debugging in OBS
- Right-click browser source → Interact → Opens browser window
- Press F12 to open DevTools → Check Console for errors
- Settings update logs show when changes are received
All APIs are within free tier limits for 24/7 streaming:
- OpenWeatherMap: ~288 calls/day (0.03% of limit)
- LocationIQ: ~1,440 calls/day (29% of limit)
- Mapbox: Cached tiles, minimal usage
Rate limiting and cooldowns are built-in to prevent quota issues.
- Next.js 16 - React framework with app router
- TypeScript - Type safety
- CSS Modules - Component-scoped styling
- MapLibre GL - WebGL-based map rendering
- Server-Sent Events - Real-time settings sync
- Custom Hooks - Reusable animation logic (
useAnimatedValue)
npm run dev # Start development server
npm run build # Build for production
npm run start # Start production server
npm run lint # Run linterGPS not updating?
- Check RTIRL pull key is correct
- Verify you're broadcasting location in RealtimeIRL app
Minimap not showing?
- Ensure you're moving >5 km/h (or set to "Always On" mode)
- Check Mapbox token is valid
- Verify GPS data is fresh (updates received within last 10 seconds)
- Check browser console for GPS staleness warnings
Weather not displaying?
- Verify OpenWeatherMap API key
- Check browser console for API errors
Admin panel won't load?
- Set
ADMIN_PASSWORDin environment variables - Restart dev server after adding env vars
Duplicate console logs in development?
- This is normal! Next.js 15 enables React StrictMode by default in development
- StrictMode intentionally renders components twice to detect side effects
- Logs will appear twice - this is expected behavior and won't happen in production
- To reduce log noise, check browser console filters or disable StrictMode (not recommended)
- Admin password is HTTP-only cookie, never exposed to client
- API keys with
NEXT_PUBLIC_prefix are safe (domain-restricted, rate-limited) - All external API tokens are read-only access
-
Readability First: Text must be legible on stream at 1080p resolution
- Use font-weight 700+ for critical information
- Double text shadows for depth and readability
- Letter spacing for better legibility
- Tabular numbers for consistent temperature display
-
International Ready: Handle all countries, languages, and formats
- Normalize country names to English for consistency
- Support international characters and long city names
- Graceful handling of missing country codes (hide section rather than show incomplete data)
- Water body detection for cruise/ocean scenarios
-
API Efficient: Minimize calls while keeping data fresh
- Location updates: Adaptive thresholds based on speed + API limit enforcement:
- Minimum 18 seconds between calls (ensures <5,000/day limit)
- Flights (>200 km/h): 1km distance threshold
- Driving (50-200 km/h): 100m distance threshold
- Walking (<50 km/h): 10m distance threshold
- Rate limiting: 1 call/second + 5,000 calls/day (enforced client-side)
- Weather updates: Every 5 minutes (time-based, not movement-based)
- Rate limiting: 50 calls/minute (well under 60/min limit)
- Aggressive caching: 30-minute validity windows for location/weather data
- Location updates: Adaptive thresholds based on speed + API limit enforcement:
-
Performance Critical: Smooth 60fps for OBS capture
- CSS transitions for smooth appearance/disappearance
- Hardware acceleration (transform: translateZ(0))
- Backface visibility optimizations
- Minimal re-renders with proper React memoization
- Smooth value animations using
requestAnimationFramewith easeOutCubic easing - Optimized animation thresholds to reduce jitter (1 km/h for speed, 1m for altitude)
-
Graceful Degradation: Always show something, even if APIs fail
- GPS Freshness: Location/weather/minimap hide when GPS update is >15 minutes old
- Data Validity: Weather and location data cached for 30 minutes even if GPS becomes stale
- Incomplete Data Handling: If LocationIQ returns country name but no country code, entire top-right section hides (prevents showing incomplete data)
- Hide incomplete data rather than show errors
- Progressive enhancement (show what you have)
- Priority: Location > Weather > Minimap
Location and weather are always shown based on the latest RTIRL data. Use the "Hidden" option in the Location Display mode to manually hide both location and weather when needed (e.g., during flights).
- Location Display Modes: Neighbourhood, City, State, Country, Custom, or Hidden
- Hidden Mode: Hides both location and weather display (useful for flights, privacy, etc.)
- Weather: Updates every 5 minutes automatically
- Timezone: Always updates from RTIRL/LocationIQ/OpenWeatherMap to ensure accurate time/date display
- Flying: High speed (>200 km/h), infrequent location updates (1km threshold), country-level display recommended
- Cruising: International waters, water body detection critical, show nearest country, ocean zoom level
- City Exploration: Frequent updates (10m threshold), detailed location names, neighborhood-level display recommended
- Transit: Medium speed (50-200 km/h), moderate update frequency (100m threshold), city-level display recommended
- Walking: Low speed (<50 km/h), frequent updates (10m threshold), neighborhood or city display
-
Neighbourhood: Use when you want maximum detail (e.g., "Downtown", "SoHo", "Shinjuku")
- Best for: Walking tours, city exploration, detailed location tracking
- Falls back to city → state → country if neighbourhood data unavailable
- Second line shows city (or state/country if city overlaps with neighbourhood)
-
City: Use for general city-level tracking (e.g., "Austin", "Tokyo", "Paris")
- Best for: General travel, transit, city-to-city movement
- Most commonly used mode
- Falls back to state → country if city data unavailable
- Second line shows state (or country if state overlaps with city)
-
State: Use for state/province-level tracking (e.g., "California", "Tokyo Prefecture", "Ontario")
- Best for: Regional tracking, state-level movement, showing administrative divisions
- Falls back to country if state data unavailable
- Second line shows country only
-
Country: Use for broad geographic tracking (e.g., "United States", "Japan", "France")
- Best for: International flights, country-level tracking
- Primary line stays empty, country shown on second line with flag
- No fallback needed (country is broadest category)
-
Custom: Use for specific landmarks or pre-planned locations
- Best for: Specific venues, custom location names, pre-planned routes
- Works without GPS data (manual entry)
- Can optionally show country/flag below custom text
- Location: Primary information, largest text (1.875rem), weight 700
- Weather: Secondary information, smaller text (1.125rem), weight 800
- Flags: 32px width for visibility, subtle border for contrast
- Icons: 24px for weather icons, drop shadows for visibility
All API limits are enforced client-side to prevent quota exhaustion:
- Per-second limit: 1 call/second (strictly enforced)
- Daily limit: 5,000 calls/day (enforced with 90% safety margin = 4,500/day max)
- Implementation:
- Dual-layer protection:
- Time gate: Minimum 18 seconds between calls (ensures ~4,800 calls/day max, safely under 5,000 limit)
- Calculation: 5,000/day = ~208/hour = ~3.5/min = 1 call every ~17.3 seconds
- Using 18 seconds provides safety margin for normal operation
- Rate limiter:
checkRateLimit('locationiq')enforces 1 call/second + daily counter- Prevents burst traffic if multiple GPS updates arrive quickly
- Tracks daily usage and blocks if daily limit (4,500/day) is reached
- Handles edge cases (rapid GPS updates, app restarts, etc.)
- Time gate: Minimum 18 seconds between calls (ensures ~4,800 calls/day max, safely under 5,000 limit)
- Combined with distance thresholds (10m/100m/1000m based on speed) to avoid unnecessary calls
- Why both layers? Time gate prevents excessive calls during normal operation, while rate limiter provides burst protection and daily limit enforcement
- Dual-layer protection:
- Usage: ~1,440 calls/day typical (well under limit even for 24/7 streaming)
- Per-minute limit: 60 calls/minute (enforced at 50 calls/minute for safety)
- Monthly limit: 1,000,000 calls/month (no daily tracking needed - very conservative usage)
- Implementation:
- Weather updates every 5 minutes (time-based, not movement-based)
- Rate limiting enforced via
checkRateLimit('openweathermap')with 2-second cooldown
- Usage: ~288 calls/day typical (0.03% of monthly limit)
- Tile source: CartoDB Voyager (day) / Dark Matter (night) - free, no API key needed
- Implementation:
- MapLibre GL automatically caches tiles - only downloads new tiles when panning to new areas
- Map updates use smooth panning (
easeTo) - no new tile downloads unless moving to new geographic area - Throttled to max 1 update per 500ms to prevent excessive map operations
- Usage: Minimal - tiles cached, only new tiles downloaded when traveling to new areas
- Note: Mapbox token not required - using free CartoDB tiles instead
- Per-second limits: Enforced immediately to prevent burst traffic
- Daily limits: Tracked and enforced for LocationIQ (most restrictive)
- Cooldown periods: Minimum time between calls (1s for LocationIQ, 2s for OpenWeatherMap)
- Adaptive thresholds: Distance-based thresholds (10m/100m/1000m) reduce API calls when moving slowly
- Time gates: Minimum intervals between calls (18s for location, 5min for weather) ensure limits aren't exceeded
-
Data Caching: Always cache data for smooth transitions (30-minute validity windows)
WEATHER_DATA_VALIDITY_TIMEOUT: 30 minutesLOCATION_DATA_VALIDITY_TIMEOUT: 30 minutesGPS_FRESHNESS_TIMEOUT: 15 minutes (when to hide location/weather)GPS_STALE_TIMEOUT: 10 seconds (when GPS data is considered stale)
-
Value Animations: Use
useAnimatedValuehook for smooth integer counting transitions- Speed: 80ms per km/h, counts through each integer (50, 51, 52...)
- Altitude: 200ms per meter, counts through each integer (100, 101, 102...)
- Heart rate: 100ms per BPM, counts through each integer (70, 71, 72...)
- All use linear easing for integers to ensure each value is visible
- Prevents jittery updates from GPS fluctuations
-
Incomplete Data Handling: Hide incomplete data rather than show errors
hasIncompleteLocationDataflag: Set when LocationIQ returns country name but no country code- Entire top-right section (location/weather/minimap) hides when flag is true
- Prevents showing "United States of America" without a flag
-
Settings Updates: Handle SSE messages correctly
- SSE messages include
typeandtimestampmetadata - Always extract only settings properties:
const { type, timestamp, ...settingsData } = data - Location re-formats immediately when settings change (uses
lastRawLocation.current)
- SSE messages include
-
API Efficiency: Use adaptive thresholds based on speed + rate limiting
- Rate limiting: Use
checkRateLimit()fromrate-limiting.ts(enforces per-second + daily limits) - Location updates: Minimum 18 seconds between calls (ensures <5,000/day LocationIQ limit)
- Distance thresholds: 10m (walking), 100m (driving), 1000m (flying) - reduces calls when moving slowly
- Track successful fetches separately from last attempt times (
lastSuccessfulWeatherFetch,lastSuccessfulLocationFetch) - Use refs for synchronous updates (GPS timestamps, API call tracking)
- Prevent concurrent API calls with
weatherFetchInProgressandlocationFetchInProgressflags
- Rate limiting: Use
-
Location Formatting: Location display updates instantly when settings change
useEffectwatchessettingsand re-formatslastRawLocation.currentimmediately- No need to wait for new GPS update to see display mode change
- Falls back gracefully if formatting fails (keeps existing display)
- Fallback Logic: Each precision level checks all fields in its category before falling back
- Neighbourhood: checks neighbourhood fields → city → state → country
- City: checks city fields → state → country (never neighbourhood)
- State: checks state fields → country (never city/neighbourhood)
- Duplicate Detection: Second line skips names that overlap with primary line
- Uses
hasOverlappingNames()to check if one name contains another - Checks all fields within category before moving to next category
- Example: "Downtown Los Angeles" (primary) → skips "Los Angeles" (city), shows "California" (state)
- Uses
- ✅ Enhanced text shadows for better readability on stream
- ✅ Increased font weights (location: 700, weather: 800)
- ✅ Larger flags (32px) and weather icons (24px) for visibility
- ✅ Adaptive location update thresholds based on speed
- ✅ Smooth CSS transitions for appearance/disappearance
- ✅ Tabular numbers for consistent temperature display
- ✅ Smooth animated value transitions (speed, altitude, heart rate)
- ✅ Optimized animation settings (easeOutCubic easing, adaptive thresholds)
- ✅ Code optimizations (reusable hooks, removed duplicate code, memoization)
-
Settings Not Updating on Overlay
- Cause: SSE messages include
typeandtimestampmetadata that must be stripped - Fix: Always extract only settings:
const { type, timestamp, ...settingsData } = data - Check: Verify
setSettings()receives cleanOverlaySettingsobject, not SSE message object
- Cause: SSE messages include
-
Location Display Mode Not Reflecting Changes
- Cause: Location state not re-formatting when settings change
- Fix: Ensure
useEffectwatchingsettingsre-formatslastRawLocation.currentimmediately - Check: Location should update instantly when display mode changes (no GPS update needed)
-
Location Showing Wrong Precision Level
- Cause: Location data might not have fields for selected precision level
- Fix: Check
getLocationByPrecision()fallback order:- Neighbourhood mode: neighbourhood → city → state → country
- City mode: city → state → country (never falls back to neighbourhood)
- State mode: state → country (never falls back to city/neighbourhood)
- Check: Verify
formatLocation()is called with correctdisplayModeparameter - Note: Fallback is intentional - if city data unavailable, shows state/country instead
-
Country Name Without Flag
- Cause: LocationIQ returned country name but no country code
- Fix: Set
hasIncompleteLocationData = trueand hide entire top-right section - Check: Never show country name without flag - better to hide section entirely
-
GPS Staleness vs Data Validity
- GPS Freshness (15 min): When to hide location/weather/minimap (GPS update age)
- Data Validity (30 min): How long cached data remains usable (weather/location cache)
- GPS Stale (10 sec): When GPS data is considered stale (no updates received)
- Important: Data can be valid (cached) even if GPS is stale - this allows instant re-display
- See "GPS Staleness Behavior" section above for complete details on how staleness is handled
-
Minimap Speed Threshold
- Actual: 5 km/h (walking pace), not 10 km/h as might be documented elsewhere
- Check:
WALKING_PACE_THRESHOLD = 5in overlay page code
-
Location Display Modes
- Actual modes:
'neighbourhood' | 'city' | 'state' | 'country' | 'custom' | 'hidden' - Note: Uses British spelling
'neighbourhood'(not'neighborhood') - Fallback hierarchy: neighbourhood → city → state → country
- Check:
LocationDisplayModetype insrc/types/settings.ts - Duplicate detection: Second line skips overlapping names (e.g., "Downtown Los Angeles" + "Los Angeles" → shows state instead)
- Actual modes:
-
Settings Sync Timing
- SSE: Primary method, instant (<1 second)
- Polling: Fallback, checks every 2 seconds
- Both: Should work, but SSE preferred for real-time updates
-
Animation Performance
- All value animations use
useAnimatedValuehook for consistency - Speed/altitude/heart rate share same animation logic (DRY principle)
- Thresholds optimized to reduce GPS jitter (1 km/h for speed, 1m for altitude)
- EaseOutCubic easing provides smooth, natural feel
- All value animations use
Reusable Components & Hooks
useAnimatedValue- Custom hook for smooth numeric value transitions- Used by speed, altitude, and heart rate displays
- Configurable thresholds, durations, and precision
- RequestAnimationFrame-based for smooth 60fps animations
Component Structure
OverlayPage- Main overlay component with GPS/weather/location logicHeartRateMonitor- Pulsoid WebSocket integration with animated BPMMapLibreMinimap- WebGL-based map rendering with day/night stylingErrorBoundary- Graceful error handling for component failures
Utilities
location-utils.ts- Location formatting, fallback logic, duplicate detectionapi-utils.ts- External API integrations (LocationIQ, OpenWeatherMap)rate-limiting.ts- API rate limit enforcement (per-second + daily limits)unit-conversions.ts- Metric/imperial conversionsfallback-utils.ts- Fallback data creation when APIs fail
- Smart location name truncation with ellipsis
- Dynamic minimap zoom based on speed
- Loading indicators during API calls
- Country name normalization to English
- Compass indicator for orientation
- Travel direction/heading display
MIT License - feel free to use and modify!
Built for the IRL streaming community 🚀