8.6 KiB
Maps V2 Settings Persistence
Maps V2 persists user settings across sessions and devices using a hybrid approach with backend API storage and localStorage fallback. Settings are shared with Maps V1 for seamless migration.
Architecture
Dual Storage Strategy
-
Primary: Backend API (
/api/v1/settings)- Settings stored in User's
settingsJSONB column - Syncs across all devices/browsers
- Requires authentication via API key
- Compatible with v1 map settings
- Settings stored in User's
-
Fallback: localStorage
- Instant save/load without network
- Browser-specific storage
- Used when backend unavailable
Settings Stored
Maps V2 shares layer visibility settings with v1 using the enabled_map_layers array:
| Frontend Setting | Backend Key | Type | Default |
|---|---|---|---|
mapStyle |
maps_v2_style |
string | 'light' |
enabledMapLayers |
enabled_map_layers |
array | ['Points', 'Routes'] |
Layer Names
The enabled_map_layers array contains layer names as strings:
'Points'- Individual location points'Routes'- Connected route lines'Heatmap'- Density heatmap'Visits'- Detected area visits'Photos'- Geotagged photos'Areas'- Defined areas'Tracks'- Saved tracks'Fog of War'- Explored areas'Scratch map'- Scratched countries
Internally, v2 converts these to boolean flags (e.g., pointsVisible, routesVisible) for easier state management, but always saves back to the shared array format.
How It Works
Initialization Flow
1. User opens Maps V2
↓
2. SettingsManager.initialize(apiKey)
↓
3. SettingsManager.sync()
↓
4. Load from backend API
↓
5. Merge with defaults
↓
6. Save to localStorage (cache)
↓
7. Return merged settings
Update Flow
User toggles Heatmap layer
↓
SettingsManager.updateSetting('heatmapEnabled', true)
↓
Convert booleans → array: ['Points', 'Routes', 'Heatmap']
↓
┌──────────────────┬──────────────────┐
│ Save to │ Save to │
│ localStorage │ Backend API │
│ (instant) │ (async) │
└──────────────────┴──────────────────┘
↓ ↓
UI updates Backend stores:
immediately { enabled_map_layers: [...] }
Format Conversion
v2 internally uses boolean flags for state management but saves/loads using v1's array format:
Loading (Array → Booleans):
// Backend returns
{ enabled_map_layers: ['Points', 'Routes', 'Heatmap'] }
// Converted to
{
pointsVisible: true,
routesVisible: true,
heatmapEnabled: true,
visitsEnabled: false,
// ... etc
}
Saving (Booleans → Array):
// v2 state
{
pointsVisible: true,
routesVisible: false,
heatmapEnabled: true
}
// Saved as
{ enabled_map_layers: ['Points', 'Heatmap'] }
API Integration
Backend Endpoints
GET /api/v1/settings
// Request
Headers: {
'Authorization': 'Bearer <api_key>'
}
// Response
{
"settings": {
"maps_v2_style": "dark",
"maps_v2_heatmap": true,
// ... other settings
},
"status": "success"
}
PATCH /api/v1/settings
// Request
Headers: {
'Authorization': 'Bearer <api_key>',
'Content-Type': 'application/json'
}
Body: {
"settings": {
"maps_v2_style": "dark",
"maps_v2_heatmap": true
}
}
// Response
{
"message": "Settings updated",
"settings": { /* updated settings */ },
"status": "success"
}
Usage Examples
Basic Usage
import { SettingsManager } from 'maps_v2/utils/settings_manager'
// Initialize with API key (done in controller)
SettingsManager.initialize(apiKey)
// Sync settings from backend on app load
const settings = await SettingsManager.sync()
// Get specific setting
const mapStyle = SettingsManager.getSetting('mapStyle')
// Update setting (saves to both localStorage and backend)
await SettingsManager.updateSetting('mapStyle', 'dark')
// Reset to defaults
SettingsManager.resetToDefaults()
In Controller
export default class extends Controller {
static values = { apiKey: String }
async connect() {
// Initialize settings manager
SettingsManager.initialize(this.apiKeyValue)
// Load settings (syncs from backend)
this.settings = await SettingsManager.sync()
// Use settings
const style = await getMapStyle(this.settings.mapStyle)
this.map = new maplibregl.Map({ style })
}
updateMapStyle(event) {
const style = event.target.value
// Automatically saves to both localStorage and backend
SettingsManager.updateSetting('mapStyle', style)
}
}
Error Handling
The settings manager handles errors gracefully:
- Backend unavailable: Falls back to localStorage
- localStorage full: Logs error, uses defaults
- Invalid settings: Merges with defaults
- Network errors: Non-blocking, localStorage still updated
// Example: Backend fails, but localStorage succeeds
SettingsManager.updateSetting('mapStyle', 'dark')
// → UI updates immediately (localStorage)
// → Backend save fails silently (logged to console)
// → User experience not interrupted
Benefits
Cross-Device Sync
Settings automatically sync when user logs in from different devices:
User enables heatmap on Desktop
↓
Backend stores setting
↓
User opens app on Mobile
↓
Settings sync from backend
↓
Heatmap enabled on Mobile too
Offline Support
Works without internet connection:
User offline
↓
Settings load from localStorage
↓
User changes settings
↓
Saves to localStorage only
↓
User goes online
↓
Next setting change syncs to backend
Performance
- Instant UI updates: localStorage writes are synchronous
- Non-blocking backend sync: API calls don't freeze UI
- Cached locally: No network request on every page load
Migration from localStorage-Only
Existing users with localStorage settings will seamlessly migrate:
1. Old user opens Maps V2
↓
2. Settings manager initializes
↓
3. Loads settings from localStorage
↓
4. Syncs with backend (first time)
↓
5. Backend stores localStorage settings
↓
6. Future sessions load from backend
Database Schema
Settings stored in users.settings JSONB column:
-- Example user settings (shared between v1 and v2)
{
"maps_v2_style": "dark",
"enabled_map_layers": ["Points", "Routes", "Heatmap", "Visits"],
// ... other settings shared by both versions
"preferred_map_layer": "OpenStreetMap",
"fog_of_war_meters": "100",
"route_opacity": 60
}
Testing
Manual Testing
-
Test Backend Sync
// In browser console SettingsManager.updateSetting('mapStyle', 'dark') // Check Network tab for PATCH /api/v1/settings -
Test Cross-Device
- Change setting on Device A
- Open Maps V2 on Device B
- Verify setting is synced
-
Test Offline
- Go offline (Network tab → Offline)
- Change settings
- Verify localStorage updated
- Go online
- Change another setting
- Verify backend receives update
Automated Testing (Future)
# spec/requests/api/v1/settings_controller_spec.rb
RSpec.describe 'Maps V2 Settings' do
it 'saves maps_v2 settings' do
patch '/api/v1/settings',
params: { settings: { maps_v2_style: 'dark' } },
headers: auth_headers
expect(user.reload.settings['maps_v2_style']).to eq('dark')
end
end
Troubleshooting
Settings Not Syncing
Check API key:
console.log('API key set:', SettingsManager.apiKey !== null)
Check network requests:
- Open DevTools → Network
- Filter for
/api/v1/settings - Verify PATCH requests after setting changes
Check backend response:
// Enable verbose logging
SettingsManager.sync().then(console.log)
Settings Reset After Reload
Possible causes:
- Backend not saving (check server logs)
- API key invalid/expired
- localStorage disabled (private browsing)
Solution:
// Clear and resync
localStorage.removeItem('dawarich-maps-v2-settings')
await SettingsManager.sync()
Future Enhancements
Possible improvements:
- Settings versioning: Migrate old setting formats
- Conflict resolution: Handle concurrent updates
- Setting presets: Save/load named presets
- Export/import: Share settings between users
- Real-time sync: WebSocket updates for multi-tab support