Remember enabled map layers for users

This commit is contained in:
Eugene Burmakin 2025-10-20 20:11:28 +02:00
parent e7884b1f4f
commit 632f389ace
6 changed files with 169 additions and 50 deletions

File diff suppressed because one or more lines are too long

View file

@ -30,7 +30,8 @@ class Api::V1::SettingsController < ApiController
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
:preferred_map_layer, :points_rendering_mode, :live_map_enabled,
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
:speed_colored_routes, :speed_color_scale, :fog_of_war_threshold
:speed_colored_routes, :speed_color_scale, :fog_of_war_threshold,
enabled_map_layers: []
)
end
end

View file

@ -463,6 +463,9 @@ export default class extends BaseController {
// Add event listeners for overlay layer changes to keep routes/tracks selector in sync
this.map.on('overlayadd', (event) => {
// Save enabled layers whenever a layer is added
this.saveEnabledLayers();
if (event.name === 'Routes') {
this.handleRouteLayerToggle('routes');
// Re-establish event handlers when routes are manually added
@ -518,6 +521,9 @@ export default class extends BaseController {
});
this.map.on('overlayremove', (event) => {
// Save enabled layers whenever a layer is removed
this.saveEnabledLayers();
if (event.name === 'Routes' || event.name === 'Tracks') {
// Don't auto-switch when layers are manually turned off
// Just update the radio button state to reflect current visibility
@ -551,9 +557,12 @@ export default class extends BaseController {
}
updatePreferredBaseLayer(selectedLayerName) {
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
fetch('/api/v1/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
settings: {
preferred_map_layer: selectedLayerName
@ -570,6 +579,68 @@ export default class extends BaseController {
});
}
saveEnabledLayers() {
const enabledLayers = [];
const layerNames = [
'Points', 'Routes', 'Tracks', 'Heatmap', 'Fog of War',
'Scratch map', 'Areas', 'Photos', 'Suggested Visits', 'Confirmed Visits'
];
const controlsLayer = {
'Points': this.markersLayer,
'Routes': this.polylinesLayer,
'Tracks': this.tracksLayer,
'Heatmap': this.heatmapLayer,
'Fog of War': this.fogOverlay,
'Scratch map': this.scratchLayerManager?.getLayer(),
'Areas': this.areasLayer,
'Photos': this.photoMarkers,
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer()
};
layerNames.forEach(name => {
const layer = controlsLayer[name];
if (layer && this.map.hasLayer(layer)) {
enabledLayers.push(name);
}
});
// Add family member layers
if (window.familyController && window.familyController.familyLayers) {
Object.keys(window.familyController.familyLayers).forEach(memberName => {
const layer = window.familyController.familyLayers[memberName];
if (layer && this.map.hasLayer(layer)) {
enabledLayers.push(memberName);
}
});
}
fetch('/api/v1/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
settings: {
enabled_map_layers: enabledLayers
},
}),
})
.then((response) => response.json())
.then((data) => {
if (data.status === 'success') {
console.log('Enabled layers saved:', enabledLayers);
} else {
console.error('Failed to save enabled layers:', data.message);
}
})
.catch(error => {
console.error('Error saving enabled layers:', error);
});
}
deletePoint(id, apiKey) {
fetch(`/api/v1/points/${id}`, {
method: 'DELETE',
@ -910,9 +981,12 @@ export default class extends BaseController {
const opacityValue = event.target.route_opacity.value.replace('%', '');
const decimalOpacity = parseFloat(opacityValue) / 100;
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
fetch('/api/v1/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
settings: {
route_opacity: decimalOpacity.toString(),
@ -1297,45 +1371,80 @@ export default class extends BaseController {
// Initialize layer visibility based on user settings or defaults
// This method sets up the initial state of overlay layers
// Note: Don't automatically add layers to map here - let the layer control and user preferences handle it
// The layer control will manage which layers are visible based on user interaction
// Get enabled layers from user settings
const enabledLayers = this.userSettings.enabled_map_layers || ['Points', 'Routes', 'Heatmap'];
console.log('Initializing layers from settings:', enabledLayers);
// Initialize photos layer if user wants it visible
if (this.userSettings.photos_enabled) {
console.log('Photos layer enabled via user settings');
const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const endDate = urlParams.get('end_at') || new Date().toISOString();
const controlsLayer = {
'Points': this.markersLayer,
'Routes': this.polylinesLayer,
'Tracks': this.tracksLayer,
'Heatmap': this.heatmapLayer,
'Fog of War': this.fogOverlay,
'Scratch map': this.scratchLayerManager?.getLayer(),
'Areas': this.areasLayer,
'Photos': this.photoMarkers,
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer()
};
console.log('Auto-fetching photos for date range:', { startDate, endDate });
fetchAndDisplayPhotos({
map: this.map,
photoMarkers: this.photoMarkers,
apiKey: this.apiKey,
startDate: startDate,
endDate: endDate,
userSettings: this.userSettings
// Add family member layers if available
if (window.familyController && window.familyController.familyLayers) {
Object.entries(window.familyController.familyLayers).forEach(([memberName, layer]) => {
controlsLayer[memberName] = layer;
});
}
// Initialize fog of war if enabled in settings
if (this.userSettings.fog_of_war_enabled) {
this.updateFog(this.markers, this.clearFogRadius, this.fogLineThreshold);
}
// Apply saved layer preferences
Object.entries(controlsLayer).forEach(([name, layer]) => {
if (!layer) return;
// Initialize visits manager functionality
// Check if any visits layers are enabled by default and load data
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
// Check if confirmed visits layer is enabled by default (it's added to map in constructor)
const confirmedVisitsEnabled = this.map.hasLayer(this.visitsManager.getConfirmedVisitCirclesLayer());
const shouldBeEnabled = enabledLayers.includes(name);
const isCurrentlyEnabled = this.map.hasLayer(layer);
console.log('Visits initialization - confirmedVisitsEnabled:', confirmedVisitsEnabled);
if (shouldBeEnabled && !isCurrentlyEnabled) {
// Add layer to map
layer.addTo(this.map);
console.log(`Enabled layer: ${name}`);
if (confirmedVisitsEnabled) {
console.log('Confirmed visits layer enabled by default - fetching visits data');
this.visitsManager.fetchAndDisplayVisits();
// Trigger special initialization for certain layers
if (name === 'Photos') {
const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const endDate = urlParams.get('end_at') || new Date().toISOString();
fetchAndDisplayPhotos({
map: this.map,
photoMarkers: this.photoMarkers,
apiKey: this.apiKey,
startDate: startDate,
endDate: endDate,
userSettings: this.userSettings
});
} else if (name === 'Fog of War') {
this.updateFog(this.markers, this.clearFogRadius, this.fogLineThreshold);
} else if (name === 'Suggested Visits' || name === 'Confirmed Visits') {
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
this.visitsManager.fetchAndDisplayVisits();
}
} else if (name === 'Scratch map') {
if (this.scratchLayerManager) {
this.scratchLayerManager.addToMap();
}
} else if (name === 'Routes') {
// Re-establish event handlers for routes layer
reestablishPolylineEventHandlers(this.polylinesLayer, this.map, this.userSettings, this.distanceUnit);
} else if (name === 'Areas') {
// Show draw control when Areas layer is enabled
if (this.drawControl && !this.map._controlCorners.topleft.querySelector('.leaflet-draw')) {
this.map.addControl(this.drawControl);
}
}
} else if (!shouldBeEnabled && isCurrentlyEnabled) {
// Remove layer from map
this.map.removeLayer(layer);
console.log(`Disabled layer: ${name}`);
}
}
});
}
toggleRightPanel() {

View file

@ -19,7 +19,8 @@ class Users::SafeSettings
'photoprism_url' => nil,
'photoprism_api_key' => nil,
'maps' => { 'distance_unit' => 'km' },
'visits_suggestions_enabled' => 'true'
'visits_suggestions_enabled' => 'true',
'enabled_map_layers' => ['Routes', 'Heatmap']
}.freeze
def initialize(settings = {})
@ -47,7 +48,8 @@ class Users::SafeSettings
distance_unit: distance_unit,
visits_suggestions_enabled: visits_suggestions_enabled?,
speed_color_scale: speed_color_scale,
fog_of_war_threshold: fog_of_war_threshold
fog_of_war_threshold: fog_of_war_threshold,
enabled_map_layers: enabled_map_layers
}
end
# rubocop:enable Metrics/MethodLength
@ -127,4 +129,8 @@ class Users::SafeSettings
def fog_of_war_threshold
settings['fog_of_war_threshold']
end
def enabled_map_layers
settings['enabled_map_layers']
end
end

View file

@ -24,21 +24,17 @@
<body class='h-screen overflow-hidden relative'>
<!-- Fixed Navbar -->
<div class='fixed w-full z-50 bg-base-100 shadow-md h-16'>
<div class='fixed w-full z-40 bg-base-100 shadow-md h-16'>
<div class='container mx-auto h-full w-full flex items-center'>
<%= render 'shared/navbar' %>
</div>
</div>
<!-- Flash Messages - Fixed below navbar -->
<div class='fixed top-16 w-full z-40 h-8'>
<div class='container mx-auto px-5'>
<%= render 'shared/flash' %>
</div>
<div class='container mx-auto px-5'>
<%= render 'shared/flash' %>
</div>
<!-- Date Navigation - Fixed below flash messages -->
<!-- Full Screen Map Container -->
<div class='absolute top-40 left-0 right-0 bottom-0 w-full z-10 overflow-auto'>
<%= yield %>

View file

@ -29,7 +29,8 @@ RSpec.describe Users::SafeSettings do
distance_unit: 'km',
visits_suggestions_enabled: true,
speed_color_scale: nil,
fog_of_war_threshold: nil
fog_of_war_threshold: nil,
enabled_map_layers: ['Routes', 'Heatmap']
}
)
end
@ -53,7 +54,8 @@ RSpec.describe Users::SafeSettings do
'photoprism_url' => 'https://photoprism.example.com',
'photoprism_api_key' => 'photoprism-key',
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' },
'visits_suggestions_enabled' => false
'visits_suggestions_enabled' => false,
'enabled_map_layers' => ['Points', 'Routes', 'Areas', 'Photos']
}
end
let(:safe_settings) { described_class.new(settings) }
@ -76,7 +78,8 @@ RSpec.describe Users::SafeSettings do
"photoprism_url" => "https://photoprism.example.com",
"photoprism_api_key" => "photoprism-key",
"maps" => { "name" => "custom", "url" => "https://custom.example.com" },
"visits_suggestions_enabled" => false
"visits_suggestions_enabled" => false,
"enabled_map_layers" => ['Points', 'Routes', 'Areas', 'Photos']
}
)
end
@ -102,7 +105,8 @@ RSpec.describe Users::SafeSettings do
distance_unit: nil,
visits_suggestions_enabled: false,
speed_color_scale: nil,
fog_of_war_threshold: nil
fog_of_war_threshold: nil,
enabled_map_layers: ['Points', 'Routes', 'Areas', 'Photos']
}
)
end
@ -132,6 +136,7 @@ RSpec.describe Users::SafeSettings do
expect(safe_settings.photoprism_api_key).to be_nil
expect(safe_settings.maps).to eq({ "distance_unit" => "km" })
expect(safe_settings.visits_suggestions_enabled?).to be true
expect(safe_settings.enabled_map_layers).to eq(['Routes', 'Heatmap'])
end
end
@ -153,7 +158,8 @@ RSpec.describe Users::SafeSettings do
'photoprism_url' => 'https://photoprism.example.com',
'photoprism_api_key' => 'photoprism-key',
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' },
'visits_suggestions_enabled' => false
'visits_suggestions_enabled' => false,
'enabled_map_layers' => ['Points', 'Tracks', 'Fog of War', 'Suggested Visits']
}
end
@ -174,6 +180,7 @@ RSpec.describe Users::SafeSettings do
expect(safe_settings.photoprism_api_key).to eq('photoprism-key')
expect(safe_settings.maps).to eq({ 'name' => 'custom', 'url' => 'https://custom.example.com' })
expect(safe_settings.visits_suggestions_enabled?).to be false
expect(safe_settings.enabled_map_layers).to eq(['Points', 'Tracks', 'Fog of War', 'Suggested Visits'])
end
end
end