dawarich/app/javascript/maps_maplibre/utils/settings_manager.js
Evgenii Burmakin c8242ce902
0.36.3 (#2013)
* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

* Pull only necessary data for map v2 points

* Feature/raw data archive (#2009)

* 0.36.2 (#2007)

* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>

* Remove esbuild scripts from package.json

* Remove sideEffects field from package.json

* Raw data archivation

* Add tests

* Fix tests

* Fix tests

* Update ExceptionReporter

* Add schedule to run raw data archival job monthly

* Change file structure for raw data archival feature

* Update changelog and version for raw data archival feature

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>

* Set raw_data to an empty hash instead of nil when archiving

* Fix storage configuration and file extraction

* Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation (#2018)

* Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation

* Remove raw data from visited cities api endpoint

* Use user timezone to show dates on maps (#2020)

* Fix/pre epoch time (#2019)

* Use user timezone to show dates on maps

* Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates.

* Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates.

* Fix tests failing due to new index on stats table

* Fix failing specs

* Update redis client configuration to support unix socket connection

* Update changelog

* Fix kml kmz import issues (#2023)

* Fix kml kmz import issues

* Refactor KML importer to improve readability and maintainability

* Implement moving points in map v2 and fix route rendering logic to ma… (#2027)

* Implement moving points in map v2 and fix route rendering logic to match map v1.

* Fix route spec

* fix(maplibre): update date format to ISO 8601 (#2029)

* Add verification step to raw data archival process (#2028)

* Add verification step to raw data archival process

* Add actual verification of raw data archives after creation, and only clear raw_data for verified archives.

* Fix failing specs

* Eliminate zip-bomb risk

* Fix potential memory leak in js

* Return .keep files

* Use Toast instead of alert for notifications

* Add help section to navbar dropdown

* Update changelog

* Remove raw_data_archival_job

* Ensure file is being closed properly after reading in Archivable concern

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>
2025-12-14 12:05:59 +01:00

308 lines
9.3 KiB
JavaScript

/**
* Settings manager for persisting user preferences
* Loads settings from backend API only (no localStorage)
*/
const DEFAULT_SETTINGS = {
mapStyle: 'light',
enabledMapLayers: ['Points', 'Routes'], // Compatible with v1 map
// Advanced settings (matching v1 naming)
routeOpacity: 0.6,
fogOfWarRadius: 100,
fogOfWarThreshold: 1,
metersBetweenRoutes: 1000,
minutesBetweenRoutes: 60,
pointsRenderingMode: 'raw',
speedColoredRoutes: false,
speedColorScale: '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
}
// Mapping between v2 layer names and v1 layer names in enabled_map_layers array
const LAYER_NAME_MAP = {
'Points': 'pointsVisible',
'Routes': 'routesVisible',
'Heatmap': 'heatmapEnabled',
'Visits': 'visitsEnabled',
'Photos': 'photosEnabled',
'Areas': 'areasEnabled',
'Tracks': 'tracksEnabled',
'Fog of War': 'fogEnabled',
'Scratch map': 'scratchEnabled'
}
// Mapping between frontend settings and backend API keys
const BACKEND_SETTINGS_MAP = {
mapStyle: 'maps_maplibre_style',
enabledMapLayers: 'enabled_map_layers',
routeOpacity: 'route_opacity',
fogOfWarRadius: 'fog_of_war_meters',
fogOfWarThreshold: 'fog_of_war_threshold',
metersBetweenRoutes: 'meters_between_routes',
minutesBetweenRoutes: 'minutes_between_routes',
pointsRenderingMode: 'points_rendering_mode',
speedColoredRoutes: 'speed_colored_routes',
speedColorScale: 'speed_color_scale'
}
export class SettingsManager {
static apiKey = null
static cachedSettings = null
/**
* Initialize settings manager with API key
* @param {string} apiKey - User's API key for backend requests
*/
static initialize(apiKey) {
this.apiKey = apiKey
this.cachedSettings = null // Clear cache on initialization
}
/**
* Get all settings from cache or defaults
* Converts enabled_map_layers array to individual boolean flags
* @returns {Object} Settings object
*/
static getSettings() {
// Return cached settings if available
if (this.cachedSettings) {
return { ...this.cachedSettings }
}
// Convert enabled_map_layers array to individual boolean flags
const expandedSettings = this._expandLayerSettings(DEFAULT_SETTINGS)
this.cachedSettings = expandedSettings
return { ...expandedSettings }
}
/**
* Convert enabled_map_layers array to individual boolean flags
* @param {Object} settings - Settings with enabledMapLayers array
* @returns {Object} Settings with individual layer booleans
*/
static _expandLayerSettings(settings) {
const enabledLayers = settings.enabledMapLayers || []
// Set boolean flags based on array contents
Object.entries(LAYER_NAME_MAP).forEach(([layerName, settingKey]) => {
settings[settingKey] = enabledLayers.includes(layerName)
})
return settings
}
/**
* Convert individual boolean flags to enabled_map_layers array
* @param {Object} settings - Settings with individual layer booleans
* @returns {Array} Array of enabled layer names
*/
static _collapseLayerSettings(settings) {
const enabledLayers = []
Object.entries(LAYER_NAME_MAP).forEach(([layerName, settingKey]) => {
if (settings[settingKey] === true) {
enabledLayers.push(layerName)
}
})
return enabledLayers
}
/**
* Load settings from backend API
* @returns {Promise<Object>} Settings object from backend
*/
static async loadFromBackend() {
if (!this.apiKey) {
console.warn('[Settings] API key not set, cannot load from backend')
return null
}
try {
const response = await fetch('/api/v1/settings', {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to load settings: ${response.status}`)
}
const data = await response.json()
const backendSettings = data.settings
// Convert backend settings to frontend format
const frontendSettings = {}
Object.entries(BACKEND_SETTINGS_MAP).forEach(([frontendKey, backendKey]) => {
if (backendKey in backendSettings) {
let value = backendSettings[backendKey]
// Convert backend values to correct types
if (frontendKey === 'routeOpacity') {
value = parseFloat(value) || DEFAULT_SETTINGS.routeOpacity
} else if (frontendKey === 'fogOfWarRadius') {
value = parseInt(value) || DEFAULT_SETTINGS.fogOfWarRadius
} else if (frontendKey === 'fogOfWarThreshold') {
value = parseInt(value) || DEFAULT_SETTINGS.fogOfWarThreshold
} else if (frontendKey === 'metersBetweenRoutes') {
value = parseInt(value) || DEFAULT_SETTINGS.metersBetweenRoutes
} else if (frontendKey === 'minutesBetweenRoutes') {
value = parseInt(value) || DEFAULT_SETTINGS.minutesBetweenRoutes
} else if (frontendKey === 'speedColoredRoutes') {
value = value === true || value === 'true'
}
frontendSettings[frontendKey] = value
}
})
// Merge with defaults
const mergedSettings = { ...DEFAULT_SETTINGS, ...frontendSettings }
// If backend has enabled_map_layers, use it as-is
if (backendSettings.enabled_map_layers) {
mergedSettings.enabledMapLayers = backendSettings.enabled_map_layers
}
// Convert enabled_map_layers array to individual boolean flags
const expandedSettings = this._expandLayerSettings(mergedSettings)
// Cache the settings
this.cachedSettings = expandedSettings
return expandedSettings
} catch (error) {
console.error('[Settings] Failed to load from backend:', error)
return null
}
}
/**
* Update cache with new settings
* @param {Object} settings - Settings object
*/
static updateCache(settings) {
this.cachedSettings = { ...settings }
}
/**
* Save settings to backend API
* @param {Object} settings - Settings to save
* @returns {Promise<boolean>} Success status
*/
static async saveToBackend(settings) {
if (!this.apiKey) {
console.warn('[Settings] API key not set, cannot save to backend')
return false
}
try {
// Convert individual layer booleans to enabled_map_layers array
const enabledMapLayers = this._collapseLayerSettings(settings)
// Convert frontend settings to backend format
const backendSettings = {}
Object.entries(BACKEND_SETTINGS_MAP).forEach(([frontendKey, backendKey]) => {
if (frontendKey === 'enabledMapLayers') {
// Use the collapsed array
backendSettings[backendKey] = enabledMapLayers
} else if (frontendKey in settings) {
let value = settings[frontendKey]
// Convert frontend values to backend format
if (frontendKey === 'routeOpacity') {
value = parseFloat(value).toString()
} else if (frontendKey === 'fogOfWarRadius' || frontendKey === 'fogOfWarThreshold' ||
frontendKey === 'metersBetweenRoutes' || frontendKey === 'minutesBetweenRoutes') {
value = parseInt(value).toString()
} else if (frontendKey === 'speedColoredRoutes') {
value = Boolean(value)
}
backendSettings[backendKey] = value
}
})
const response = await fetch('/api/v1/settings', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ settings: backendSettings })
})
if (!response.ok) {
throw new Error(`Failed to save settings: ${response.status}`)
}
return true
} catch (error) {
console.error('[Settings] Failed to save to backend:', error)
return false
}
}
/**
* Get a specific setting
* @param {string} key - Setting key
* @returns {*} Setting value
*/
static getSetting(key) {
return this.getSettings()[key]
}
/**
* Update a specific setting and save to backend
* @param {string} key - Setting key
* @param {*} value - New value
*/
static async updateSetting(key, value) {
const settings = this.getSettings()
settings[key] = value
// If this is a layer visibility setting, also update the enabledMapLayers array
// This ensures the array is in sync before backend save
const isLayerSetting = Object.values(LAYER_NAME_MAP).includes(key)
if (isLayerSetting) {
settings.enabledMapLayers = this._collapseLayerSettings(settings)
}
// Update cache immediately
this.updateCache(settings)
// Save to backend
await this.saveToBackend(settings)
}
/**
* Reset to defaults
*/
static async resetToDefaults() {
try {
this.cachedSettings = null // Clear cache
// Reset on backend
if (this.apiKey) {
await this.saveToBackend(DEFAULT_SETTINGS)
}
} catch (error) {
console.error('Failed to reset settings:', error)
}
}
/**
* Sync settings: load from backend
* Call this on app initialization
* @returns {Promise<Object>} Settings from backend
*/
static async sync() {
const backendSettings = await this.loadFromBackend()
if (backendSettings) {
return backendSettings
}
return this.getSettings()
}
}