Apply global map theme updates and UI improvements

This commit is contained in:
Smittix
2026-02-20 00:32:58 +00:00
parent 963bcdf9fa
commit 1466fc2d30
22 changed files with 1365 additions and 351 deletions

View File

@@ -22,15 +22,16 @@ const Settings = {
cartodb_dark: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd'
subdomains: 'abcd',
mapTheme: 'cyber',
options: {}
},
cartodb_dark_cyan: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
options: {
className: 'tile-layer-cyan'
}
mapTheme: 'cyber',
options: {}
},
cartodb_light: {
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
@@ -50,26 +51,153 @@ const Settings = {
// Current settings cache
_cache: {},
// Init guard to prevent concurrent fetch races across pages/modes
_initialized: false,
_initPromise: null,
_themeObserver: null,
_themeObserverStarted: false,
_themeObserverRaf: null,
/**
* Check if a tile provider key is valid.
* @param {string} provider
* @returns {boolean}
*/
_isKnownTileProvider(provider) {
if (typeof provider !== 'string') return false;
const key = provider.trim();
return key === 'custom' || Object.prototype.hasOwnProperty.call(this.tileProviders, key);
},
/**
* Normalize tile provider values from storage/UI.
* @param {string} provider
* @returns {string}
*/
_normalizeTileProvider(provider) {
if (typeof provider !== 'string') return this.defaults['offline.tile_provider'];
const key = provider.trim();
if (this._isKnownTileProvider(key)) return key;
return this.defaults['offline.tile_provider'];
},
/**
* Persist and retrieve preferred map theme behavior for dark Carto tiles.
* Helps keep Cyber style enabled even if server-side tile provider drifts.
*/
_getMapThemePreference() {
if (typeof localStorage === 'undefined') return 'cyber';
const pref = localStorage.getItem('intercept_map_theme_pref');
if (pref === 'none' || pref === 'cyber') return pref;
return 'cyber';
},
_setMapThemePreference(pref) {
if (typeof localStorage === 'undefined') return;
if (pref !== 'none' && pref !== 'cyber') return;
localStorage.setItem('intercept_map_theme_pref', pref);
},
/**
* Whether Cyber map theme should be considered active globally.
* @param {Object} [config]
* @returns {boolean}
*/
_isCyberThemeEnabled(config) {
const resolvedConfig = config || this.getTileConfig();
return this._getMapThemeClass(resolvedConfig) === 'map-theme-cyber';
},
/**
* Toggle root class used for hard global Leaflet theming.
* @param {Object} [config]
*/
_syncRootMapThemeClass(config) {
if (typeof document === 'undefined' || !document.documentElement) return;
const enabled = this._isCyberThemeEnabled(config);
document.documentElement.classList.toggle('map-cyber-enabled', enabled);
},
/**
* Prefer localStorage tile settings when available to avoid stale server values.
*/
_applyLocalTileOverrides() {
const stored = localStorage.getItem('intercept_settings');
if (!stored) return;
try {
const local = JSON.parse(stored) || {};
const localProvider = this._normalizeTileProvider(local['offline.tile_provider']);
if (localProvider) {
this._cache['offline.tile_provider'] = localProvider;
}
if (typeof local['offline.tile_server_url'] === 'string') {
this._cache['offline.tile_server_url'] = local['offline.tile_server_url'];
}
} catch (e) {
// Ignore malformed local settings and keep current cache.
}
},
/**
* Initialize settings - load from server/localStorage
*/
async init() {
try {
const response = await fetch('/offline/settings');
if (response.ok) {
const data = await response.json();
this._cache = { ...this.defaults, ...data.settings };
} else {
// Fall back to localStorage
this._loadFromLocalStorage();
}
} catch (e) {
console.warn('Failed to load settings from server, using localStorage:', e);
this._loadFromLocalStorage();
async init(options = {}) {
const force = Boolean(options && options.force);
if (!force && this._initialized) {
return this._cache;
}
this._updateUI();
return this._cache;
if (!force && this._initPromise) {
return this._initPromise;
}
this._initPromise = (async () => {
try {
const response = await fetch('/offline/settings');
if (response.ok) {
const data = await response.json();
this._cache = { ...this.defaults, ...data.settings };
} else {
// Fall back to localStorage
this._loadFromLocalStorage();
}
} catch (e) {
console.warn('Failed to load settings from server, using localStorage:', e);
this._loadFromLocalStorage();
}
this._applyLocalTileOverrides();
this._cache['offline.tile_provider'] = this._normalizeTileProvider(this._cache['offline.tile_provider']);
// If dark Carto was restored by stale server settings but user prefers Cyber,
// keep the visible provider aligned with Cyber selection.
if (this._cache['offline.tile_provider'] === 'cartodb_dark' && this._getMapThemePreference() === 'cyber') {
this._cache['offline.tile_provider'] = 'cartodb_dark_cyan';
}
this._updateUI();
// Re-apply map theme to already-registered maps in case init happened after map creation.
const allMaps = this._collectMaps();
if (allMaps.length > 0) {
const config = this.getTileConfig();
allMaps.forEach((map) => this._applyMapTheme(map, config));
}
const activeConfig = this.getTileConfig();
this._syncRootMapThemeClass(activeConfig);
this._applyThemeToAllContainers(activeConfig);
this._ensureThemeObserver();
this._initialized = true;
return this._cache;
})();
try {
return await this._initPromise;
} finally {
this._initPromise = null;
}
},
/**
@@ -99,11 +227,14 @@ const Settings = {
// Save to server
try {
await fetch('/offline/settings', {
const response = await fetch('/offline/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value })
});
if (!response.ok) {
throw new Error(`Save failed (${response.status})`);
}
} catch (e) {
console.warn('Failed to save setting to server:', e);
}
@@ -152,6 +283,16 @@ const Settings = {
* Set tile provider
*/
async setTileProvider(provider) {
provider = this._normalizeTileProvider(provider);
if (provider === 'cartodb_dark_cyan') {
this._setMapThemePreference('cyber');
} else if (provider === 'cartodb_dark') {
this._setMapThemePreference('none');
} else {
this._setMapThemePreference('none');
}
await this._save('offline.tile_provider', provider);
// Show/hide custom URL input
@@ -160,10 +301,11 @@ const Settings = {
customRow.style.display = provider === 'custom' ? 'block' : 'none';
}
// If not custom and we have a map, update tiles immediately
if (provider !== 'custom') {
this._updateMapTiles();
}
// Update tiles immediately for all providers.
this._updateMapTiles();
const activeConfig = this.getTileConfig();
this._syncRootMapThemeClass(activeConfig);
this._applyThemeToAllContainers(activeConfig);
},
/**
@@ -178,7 +320,7 @@ const Settings = {
* Get current tile configuration
*/
getTileConfig() {
const provider = this.get('offline.tile_provider');
const provider = this._normalizeTileProvider(this.get('offline.tile_provider'));
if (provider === 'custom') {
const customUrl = this.get('offline.tile_server_url');
@@ -189,7 +331,170 @@ const Settings = {
};
}
return this.tileProviders[provider] || this.tileProviders.cartodb_dark;
const config = this.tileProviders[provider] || this.tileProviders.cartodb_dark;
// Robust fallback: if dark Carto is active and Cyber is preferred,
// keep Cyber theme enabled even when provider temporarily reverts.
if (provider === 'cartodb_dark' && this._getMapThemePreference() === 'cyber') {
return { ...config, mapTheme: 'cyber' };
}
return config;
},
/**
* Resolve map theme class from tile config.
* @param {Object} config
* @returns {string|null}
*/
_getMapThemeClass(config) {
if (!config || !config.mapTheme) return null;
if (config.mapTheme === 'cyber') return 'map-theme-cyber';
return null;
},
/**
* Apply or clear map theme styles for a Leaflet container.
* @param {HTMLElement} container
* @param {Object} [config]
*/
_applyThemeToContainer(container, config) {
if (!container || !container.classList) return;
const tilePane = container.querySelector('.leaflet-tile-pane');
container.querySelectorAll('.intercept-map-theme-overlay').forEach((el) => el.remove());
if (tilePane && tilePane.style) {
tilePane.style.filter = '';
tilePane.style.opacity = '';
tilePane.style.willChange = '';
}
if (container.style) {
container.style.background = '';
}
container.classList.remove('map-theme-cyber');
const resolvedConfig = config || this.getTileConfig();
const themeClass = this._getMapThemeClass(resolvedConfig);
if (!themeClass) return;
container.classList.add(themeClass);
if (container.style) {
container.style.background = '#020813';
}
if (tilePane && tilePane.style) {
tilePane.style.filter = 'sepia(0.74) hue-rotate(176deg) saturate(1.72) brightness(1.05) contrast(1.08)';
tilePane.style.opacity = '1';
tilePane.style.willChange = 'filter';
}
// Grid/glow overlays are rendered via CSS pseudo elements on
// `html.map-cyber-enabled .leaflet-container` for consistent stacking.
},
/**
* Apply/remove map theme class on a Leaflet map container.
* @param {L.Map} map
* @param {Object} [config]
*/
_applyMapTheme(map, config) {
if (!map || typeof map.getContainer !== 'function') return;
const container = map.getContainer();
this._applyThemeToContainer(container, config);
},
/**
* Apply current map theme to all rendered Leaflet containers.
* Covers maps that were not explicitly registered with Settings.
* @param {Object} [config]
*/
_applyThemeToAllContainers(config) {
if (typeof document === 'undefined') return;
const containers = document.querySelectorAll('.leaflet-container');
if (!containers.length) return;
const resolvedConfig = config || this.getTileConfig();
this._syncRootMapThemeClass(resolvedConfig);
containers.forEach((container) => this._applyThemeToContainer(container, resolvedConfig));
},
/**
* Watch the DOM for new Leaflet maps and apply current theme automatically.
*/
_ensureThemeObserver() {
if (this._themeObserverStarted || typeof MutationObserver === 'undefined') return;
if (typeof document === 'undefined' || !document.body) return;
const scheduleApply = () => {
if (this._themeObserverRaf && typeof cancelAnimationFrame === 'function') {
cancelAnimationFrame(this._themeObserverRaf);
}
if (typeof requestAnimationFrame === 'function') {
this._themeObserverRaf = requestAnimationFrame(() => {
this._themeObserverRaf = null;
this._applyThemeToAllContainers(this.getTileConfig());
});
} else {
this._applyThemeToAllContainers(this.getTileConfig());
}
};
this._themeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!mutation.addedNodes || mutation.addedNodes.length === 0) continue;
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.classList.contains('leaflet-container') || node.querySelector('.leaflet-container')) {
scheduleApply();
return;
}
}
}
});
this._themeObserver.observe(document.body, {
childList: true,
subtree: true
});
this._themeObserverStarted = true;
},
/**
* Collect all known map instances.
* @returns {L.Map[]}
*/
_collectMaps() {
const windowMaps = [
window.map,
window.leafletMap,
window.aprsMap,
window.radarMap,
window.vesselMap,
window.groundMap,
window.groundTrackMap,
window.meshMap,
window.issMap
].filter(m => m && typeof m.eachLayer === 'function');
return [...new Set([...this._registeredMaps, ...windowMaps])];
},
/**
* Keep map theme stable if map internals or layers are refreshed.
* @param {L.Map} map - Leaflet map instance
*/
_attachMapThemeHooks(map) {
if (!map || typeof map.on !== 'function' || map._interceptThemeHookBound) return;
const reapplyTheme = () => this._applyMapTheme(map);
const hookEvents = ['layeradd', 'layerremove', 'zoomend', 'resize', 'load'];
hookEvents.forEach((eventName) => map.on(eventName, reapplyTheme));
map._interceptThemeHookBound = true;
map._interceptThemeHookHandler = reapplyTheme;
},
/**
@@ -200,6 +505,18 @@ const Settings = {
if (map && typeof map.eachLayer === 'function' && !this._registeredMaps.includes(map)) {
this._registeredMaps.push(map);
}
this._ensureThemeObserver();
this._attachMapThemeHooks(map);
this._applyMapTheme(map);
this._applyThemeToAllContainers(this.getTileConfig());
// Some maps create tile DOM asynchronously; re-apply after first paint.
if (typeof window !== 'undefined' && typeof window.setTimeout === 'function') {
window.setTimeout(() => {
this._applyMapTheme(map);
this._applyThemeToAllContainers(this.getTileConfig());
}, 120);
}
},
/**
@@ -211,6 +528,15 @@ const Settings = {
if (idx > -1) {
this._registeredMaps.splice(idx, 1);
}
if (map && map._interceptThemeHookBound && typeof map.off === 'function') {
const handler = map._interceptThemeHookHandler;
['layeradd', 'layerremove', 'zoomend', 'resize', 'load'].forEach((eventName) => {
map.off(eventName, handler);
});
delete map._interceptThemeHookBound;
delete map._interceptThemeHookHandler;
}
},
/**
@@ -341,25 +667,11 @@ const Settings = {
* Update map tiles on all known maps
*/
_updateMapTiles() {
// Combine registered maps with common window map variables
const windowMaps = [
window.map,
window.leafletMap,
window.aprsMap,
window.radarMap,
window.vesselMap,
window.groundMap,
window.groundTrackMap,
window.meshMap,
window.issMap
].filter(m => m && typeof m.eachLayer === 'function');
// Combine with registered maps, removing duplicates
const allMaps = [...new Set([...this._registeredMaps, ...windowMaps])];
const allMaps = this._collectMaps();
if (allMaps.length === 0) return;
const config = this.getTileConfig();
this._syncRootMapThemeClass(config);
allMaps.forEach(map => {
// Remove existing tile layers
@@ -380,7 +692,10 @@ const Settings = {
}
L.tileLayer(config.url, options).addTo(map);
this._applyMapTheme(map, config);
});
this._applyThemeToAllContainers(config);
},
/**
@@ -572,12 +887,6 @@ function loadSettingsTools() {
});
}
// Initialize settings on page load
document.addEventListener('DOMContentLoaded', () => {
Settings.init();
switchSettingsTab('offline');
});
// =============================================================================
// Location Settings Functions
// =============================================================================