mirror of
https://github.com/Freika/dawarich.git
synced 2026-05-04 11:39:24 -05:00
Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import "leaflet.heat";
|
||||
import "leaflet.control.layers.tree";
|
||||
import consumer from "../channels/consumer";
|
||||
|
||||
import { createMarkersArray } from "../maps/markers";
|
||||
@@ -45,7 +46,11 @@ import { TileMonitor } from "../maps/tile_monitor";
|
||||
import BaseController from "./base_controller";
|
||||
import { createAllMapLayers } from "../maps/layers";
|
||||
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
|
||||
import { addTopRightButtons } from "../maps/map_controls";
|
||||
import {
|
||||
addTopRightButtons,
|
||||
setCreatePlaceButtonActive,
|
||||
setCreatePlaceButtonInactive
|
||||
} from "../maps/map_controls";
|
||||
|
||||
export default class extends BaseController {
|
||||
static targets = ["container"];
|
||||
@@ -218,6 +223,14 @@ export default class extends BaseController {
|
||||
this.placesManager = new PlacesManager(this.map, this.apiKey);
|
||||
this.placesManager.initialize();
|
||||
|
||||
// Parse user tags for places layer control
|
||||
try {
|
||||
this.userTags = this.element.dataset.user_tags ? JSON.parse(this.element.dataset.user_tags) : [];
|
||||
} catch (error) {
|
||||
console.error('Error parsing user tags:', error);
|
||||
this.userTags = [];
|
||||
}
|
||||
|
||||
// Expose maps controller globally for family integration
|
||||
window.mapsController = this;
|
||||
|
||||
@@ -234,9 +247,6 @@ export default class extends BaseController {
|
||||
}
|
||||
this.switchRouteMode('routes', true);
|
||||
|
||||
// Initialize layers based on settings
|
||||
this.initializeLayersFromSettings();
|
||||
|
||||
// Listen for Family Members layer becoming ready
|
||||
this.setupFamilyLayerListener();
|
||||
|
||||
@@ -252,22 +262,12 @@ export default class extends BaseController {
|
||||
// Add all top-right buttons in the correct order
|
||||
this.initializeTopRightButtons();
|
||||
|
||||
// Initialize layers for the layer control
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Routes: this.polylinesLayer,
|
||||
Tracks: this.tracksLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer,
|
||||
Photos: this.photoMarkers,
|
||||
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
||||
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer(),
|
||||
"Places": this.placesManager.placesLayer
|
||||
};
|
||||
// Initialize tree-based layer control (must be before initializeLayersFromSettings)
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
// Initialize layers based on settings (must be after tree control creation)
|
||||
this.initializeLayersFromSettings();
|
||||
|
||||
|
||||
// Initialize Live Map Handler
|
||||
@@ -447,6 +447,134 @@ export default class extends BaseController {
|
||||
return maps;
|
||||
}
|
||||
|
||||
createTreeLayerControl(additionalLayers = {}) {
|
||||
// Build base maps tree structure
|
||||
const baseMapsTree = {
|
||||
label: 'Map Styles',
|
||||
children: []
|
||||
};
|
||||
|
||||
const maps = this.baseMaps();
|
||||
Object.entries(maps).forEach(([name, layer]) => {
|
||||
baseMapsTree.children.push({
|
||||
label: name,
|
||||
layer: layer
|
||||
});
|
||||
});
|
||||
|
||||
// Build places subtree with tags
|
||||
// Store filtered layers for later restoration
|
||||
if (!this.placesFilteredLayers) {
|
||||
this.placesFilteredLayers = {};
|
||||
}
|
||||
|
||||
// Create Untagged layer
|
||||
const untaggedLayer = this.placesManager?.createFilteredLayer([]) || L.layerGroup();
|
||||
this.placesFilteredLayers['Untagged'] = untaggedLayer;
|
||||
|
||||
const placesChildren = [
|
||||
{
|
||||
label: 'Untagged',
|
||||
layer: untaggedLayer
|
||||
}
|
||||
];
|
||||
|
||||
// Add individual tag layers
|
||||
if (this.userTags && this.userTags.length > 0) {
|
||||
this.userTags.forEach(tag => {
|
||||
const icon = tag.icon || '📍';
|
||||
const label = `${icon} ${tag.name}`;
|
||||
const tagLayer = this.placesManager?.createFilteredLayer([tag.id]) || L.layerGroup();
|
||||
this.placesFilteredLayers[label] = tagLayer;
|
||||
placesChildren.push({
|
||||
label: label,
|
||||
layer: tagLayer
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Build visits subtree
|
||||
const visitsChildren = [
|
||||
{
|
||||
label: 'Suggested',
|
||||
layer: this.visitsManager?.getVisitCirclesLayer() || L.layerGroup()
|
||||
},
|
||||
{
|
||||
label: 'Confirmed',
|
||||
layer: this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
||||
}
|
||||
];
|
||||
|
||||
// Build the overlays tree structure
|
||||
const overlaysTree = {
|
||||
label: 'Layers',
|
||||
selectAllCheckbox: false,
|
||||
children: [
|
||||
{
|
||||
label: 'Points',
|
||||
layer: this.markersLayer
|
||||
},
|
||||
{
|
||||
label: 'Routes',
|
||||
layer: this.polylinesLayer
|
||||
},
|
||||
{
|
||||
label: 'Tracks',
|
||||
layer: this.tracksLayer
|
||||
},
|
||||
{
|
||||
label: 'Heatmap',
|
||||
layer: this.heatmapLayer
|
||||
},
|
||||
{
|
||||
label: 'Fog of War',
|
||||
layer: this.fogOverlay
|
||||
},
|
||||
{
|
||||
label: 'Scratch map',
|
||||
layer: this.scratchLayerManager?.getLayer() || L.layerGroup()
|
||||
},
|
||||
{
|
||||
label: 'Areas',
|
||||
layer: this.areasLayer
|
||||
},
|
||||
{
|
||||
label: 'Photos',
|
||||
layer: this.photoMarkers
|
||||
},
|
||||
{
|
||||
label: 'Visits',
|
||||
selectAllCheckbox: true,
|
||||
children: visitsChildren
|
||||
},
|
||||
{
|
||||
label: 'Places',
|
||||
selectAllCheckbox: true,
|
||||
children: placesChildren
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Add Family Members layer if available
|
||||
if (additionalLayers['Family Members']) {
|
||||
overlaysTree.children.push({
|
||||
label: 'Family Members',
|
||||
layer: additionalLayers['Family Members']
|
||||
});
|
||||
}
|
||||
|
||||
// Create the tree control
|
||||
return L.control.layers.tree(
|
||||
baseMapsTree,
|
||||
overlaysTree,
|
||||
{
|
||||
namedToggle: false,
|
||||
collapsed: true,
|
||||
position: 'topright'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeEventListeners() {
|
||||
document.removeEventListener('click', this.handleDeleteClick);
|
||||
}
|
||||
@@ -572,6 +700,15 @@ export default class extends BaseController {
|
||||
this.fogOverlay = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for place creation events to disable creation mode
|
||||
document.addEventListener('place:created', () => {
|
||||
this.disablePlaceCreationMode();
|
||||
});
|
||||
|
||||
document.addEventListener('place:create:cancelled', () => {
|
||||
this.disablePlaceCreationMode();
|
||||
});
|
||||
}
|
||||
|
||||
updatePreferredBaseLayer(selectedLayerName) {
|
||||
@@ -599,32 +736,23 @@ 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',
|
||||
'Family Members'
|
||||
];
|
||||
|
||||
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(),
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer
|
||||
};
|
||||
|
||||
layerNames.forEach(name => {
|
||||
const layer = controlsLayer[name];
|
||||
if (layer && this.map.hasLayer(layer)) {
|
||||
enabledLayers.push(name);
|
||||
}
|
||||
});
|
||||
// Get all checked inputs from the tree control
|
||||
const layerControl = document.querySelector('.leaflet-control-layers');
|
||||
if (layerControl) {
|
||||
const inputs = layerControl.querySelectorAll('input[type="checkbox"]:checked');
|
||||
inputs.forEach(input => {
|
||||
// Get the label text for this checkbox
|
||||
const label = input.closest('label') || input.nextElementSibling;
|
||||
if (label) {
|
||||
const layerName = label.textContent.trim();
|
||||
// Skip group headers that might have checkboxes
|
||||
if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) {
|
||||
enabledLayers.push(layerName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/api/v1/settings', {
|
||||
method: 'PATCH',
|
||||
@@ -642,7 +770,7 @@ export default class extends BaseController {
|
||||
.then((data) => {
|
||||
if (data.status === 'success') {
|
||||
console.log('Enabled layers saved:', enabledLayers);
|
||||
showFlashMessage('notice', 'Map layer preferences saved');
|
||||
// showFlashMessage('notice', 'Map layer preferences saved');
|
||||
} else {
|
||||
console.error('Failed to save enabled layers:', data.message);
|
||||
showFlashMessage('error', `Failed to save layer preferences: ${data.message}`);
|
||||
@@ -699,16 +827,8 @@ export default class extends BaseController {
|
||||
// Update the layer control
|
||||
if (this.layerControl) {
|
||||
this.map.removeControl(this.layerControl);
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.layerGroup(),
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup()
|
||||
};
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
}
|
||||
|
||||
// Update heatmap
|
||||
@@ -1280,7 +1400,8 @@ export default class extends BaseController {
|
||||
};
|
||||
|
||||
// Re-add the layer control in the same position
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
|
||||
// Restore layer visibility states
|
||||
Object.entries(layerStates).forEach(([name, wasVisible]) => {
|
||||
@@ -1321,7 +1442,7 @@ export default class extends BaseController {
|
||||
|
||||
initializeTopRightButtons() {
|
||||
// Add all top-right buttons in the correct order:
|
||||
// 1. Select Area, 2. Add Visit, 3. Open Calendar, 4. Open Drawer
|
||||
// 1. Select Area, 2. Add Visit, 3. Create Place, 4. Open Calendar, 5. Open Drawer
|
||||
// Note: Layer control is added separately and appears at the top
|
||||
|
||||
this.topRightControls = addTopRightButtons(
|
||||
@@ -1330,6 +1451,7 @@ export default class extends BaseController {
|
||||
onSelectArea: () => this.visitsManager.toggleSelectionMode(),
|
||||
// onAddVisit is intentionally null - the add_visit_controller will attach its handler
|
||||
onAddVisit: null,
|
||||
onCreatePlace: () => this.togglePlaceCreationMode(),
|
||||
onToggleCalendar: () => this.toggleRightPanel(),
|
||||
onToggleDrawer: () => this.visitsManager.toggleDrawer()
|
||||
},
|
||||
@@ -1534,7 +1656,9 @@ export default class extends BaseController {
|
||||
'Photos': this.photoMarkers,
|
||||
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
|
||||
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer(),
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer,
|
||||
// Add Places filtered layers
|
||||
...this.placesFilteredLayers || {}
|
||||
};
|
||||
|
||||
// Apply saved layer preferences
|
||||
@@ -1606,6 +1730,38 @@ export default class extends BaseController {
|
||||
console.log(`Disabled layer: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Update the tree control checkboxes to reflect the layer states
|
||||
// Wait a bit for the tree control to be fully initialized
|
||||
setTimeout(() => {
|
||||
this.updateTreeControlCheckboxes(enabledLayers);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
updateTreeControlCheckboxes(enabledLayers) {
|
||||
const layerControl = document.querySelector('.leaflet-control-layers');
|
||||
if (!layerControl) {
|
||||
console.log('Layer control not found, skipping checkbox update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and check/uncheck all layer checkboxes based on saved state
|
||||
const inputs = layerControl.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.closest('label') || input.nextElementSibling;
|
||||
if (label) {
|
||||
const layerName = label.textContent.trim();
|
||||
const shouldBeEnabled = enabledLayers.includes(layerName);
|
||||
|
||||
// Skip group headers that might have checkboxes
|
||||
if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) {
|
||||
if (shouldBeEnabled !== input.checked) {
|
||||
input.checked = shouldBeEnabled;
|
||||
console.log(`Updated checkbox for ${layerName}: ${shouldBeEnabled}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupFamilyLayerListener() {
|
||||
@@ -2155,71 +2311,12 @@ export default class extends BaseController {
|
||||
updateLayerControl(additionalLayers = {}) {
|
||||
if (!this.layerControl) return;
|
||||
|
||||
// Store which base and overlay layers are currently visible
|
||||
const overlayStates = {};
|
||||
let activeBaseLayer = null;
|
||||
let activeBaseLayerName = null;
|
||||
|
||||
if (this.layerControl._layers) {
|
||||
Object.values(this.layerControl._layers).forEach(layerObj => {
|
||||
if (layerObj.overlay && layerObj.layer) {
|
||||
// Store overlay layer states
|
||||
overlayStates[layerObj.name] = this.map.hasLayer(layerObj.layer);
|
||||
} else if (!layerObj.overlay && this.map.hasLayer(layerObj.layer)) {
|
||||
// Store the currently active base layer
|
||||
activeBaseLayer = layerObj.layer;
|
||||
activeBaseLayerName = layerObj.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove existing layer control
|
||||
this.map.removeControl(this.layerControl);
|
||||
|
||||
// Create base controls layer object
|
||||
const baseControlsLayer = {
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Tracks: this.tracksLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup(),
|
||||
"Suggested Visits": this.visitsManager?.getVisitCirclesLayer() || L.layerGroup(),
|
||||
"Confirmed Visits": this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
||||
};
|
||||
|
||||
// Merge with additional layers (like family members)
|
||||
const controlsLayer = { ...baseControlsLayer, ...additionalLayers };
|
||||
|
||||
// Get base maps and re-add the layer control
|
||||
const baseMaps = this.baseMaps();
|
||||
this.layerControl = L.control.layers(baseMaps, controlsLayer).addTo(this.map);
|
||||
|
||||
// Restore the active base layer if we had one
|
||||
if (activeBaseLayer && activeBaseLayerName) {
|
||||
console.log(`Restoring base layer: ${activeBaseLayerName}`);
|
||||
// Make sure the base layer is added to the map
|
||||
if (!this.map.hasLayer(activeBaseLayer)) {
|
||||
activeBaseLayer.addTo(this.map);
|
||||
}
|
||||
} else {
|
||||
// If no active base layer was found, ensure we have a default one
|
||||
console.log('No active base layer found, adding default');
|
||||
const defaultBaseLayer = Object.values(baseMaps)[0];
|
||||
if (defaultBaseLayer && !this.map.hasLayer(defaultBaseLayer)) {
|
||||
defaultBaseLayer.addTo(this.map);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore overlay layer visibility states
|
||||
Object.entries(overlayStates).forEach(([name, wasVisible]) => {
|
||||
const layer = controlsLayer[name];
|
||||
if (layer && wasVisible && !this.map.hasLayer(layer)) {
|
||||
layer.addTo(this.map);
|
||||
}
|
||||
});
|
||||
// Re-add the layer control with additional layers
|
||||
this.layerControl = this.createTreeLayerControl(additionalLayers);
|
||||
this.map.addControl(this.layerControl);
|
||||
}
|
||||
|
||||
togglePlaceCreationMode() {
|
||||
@@ -2234,20 +2331,33 @@ export default class extends BaseController {
|
||||
// Disable creation mode
|
||||
this.placesManager.disableCreationMode();
|
||||
if (button) {
|
||||
button.classList.remove('btn-error');
|
||||
button.classList.add('btn-success');
|
||||
button.title = 'Click to create a place on the map';
|
||||
setCreatePlaceButtonInactive(button, this.userTheme);
|
||||
button.setAttribute('data-tip', 'Create a place');
|
||||
}
|
||||
} else {
|
||||
// Enable creation mode
|
||||
this.placesManager.enableCreationMode();
|
||||
if (button) {
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-error');
|
||||
button.title = 'Click map to place marker (click again to cancel)';
|
||||
setCreatePlaceButtonActive(button);
|
||||
button.setAttribute('data-tip', 'Click map to place marker (click to cancel)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disablePlaceCreationMode() {
|
||||
if (!this.placesManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only disable if currently in creation mode
|
||||
if (this.placesManager.creationMode) {
|
||||
this.placesManager.disableCreationMode();
|
||||
|
||||
const button = document.getElementById('create-place-btn');
|
||||
if (button) {
|
||||
setCreatePlaceButtonInactive(button, this.userTheme);
|
||||
button.setAttribute('data-tip', 'Create a place');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user