From a15c8e0b36296e1e8c8af20081238d1802243de7 Mon Sep 17 00:00:00 2001 From: livio Date: Sun, 4 Oct 2026 23:46:22 +0200 Subject: [PATCH] Add draggable compass and restore project fiducials --- Components/Pages/Home.razor | 12 +-- LMEMappeCatastaliApp.csproj | 4 +- wwwroot/css/app.css | 61 +++++++++++--- wwwroot/js/lme-map.js | 157 +++++++++++++++++++++++++++++++++--- 4 files changed, 203 insertions(+), 31 deletions(-) diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index 1aa5ca8..f3ecf41 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -680,8 +680,8 @@ @@ -889,7 +889,7 @@ private bool GpsOverlayCollapsed { get; set; } private bool ShowGpsPosition { get; set; } = true; private bool ShowCompass { get; set; } - private string CompassMode { get; set; } = "heading"; + private string CompassMode { get; set; } = "fixed"; private bool CompassSensorSubscribed { get; set; } private bool ShowSelectedParcels { get; set; } = true; private bool ShowFiducialPoints { get; set; } = true; @@ -2322,7 +2322,7 @@ DrawToolsVisible = options.DrawToolsVisible; ShowGpsPosition = options.GpsPositionVisible; ShowCompass = options.CompassVisible; - CompassMode = options.CompassMode is "fixed" ? "fixed" : "heading"; + CompassMode = options.CompassMode is "heading" ? "heading" : "fixed"; ShowSelectedParcels = options.SelectedParcelsVisible; TrailEnabled = options.TrailEnabled; @@ -2745,7 +2745,7 @@ DrawToolsVisible = GetFlagPreference("draw_tools_visible", false); ShowGpsPosition = GetFlagPreference("show_gps_position", true); ShowCompass = GetFlagPreference("show_compass", false); - CompassMode = Preferences.Default.Get($"{FlagPreferencePrefix}compass_mode", "heading") is "fixed" ? "fixed" : "heading"; + CompassMode = Preferences.Default.Get($"{FlagPreferencePrefix}compass_mode", "fixed") is "heading" ? "heading" : "fixed"; ShowSelectedParcels = GetFlagPreference("show_selected_parcels", true); ShowFiducialPoints = GetFlagPreference("show_fiducial_points", true); KeepPreviousFiducialPoints = GetFlagPreference("keep_previous_fiducials", true); @@ -2853,7 +2853,7 @@ private async Task ApplyCompassOptions() { - CompassMode = CompassMode is "fixed" ? "fixed" : "heading"; + CompassMode = CompassMode is "heading" ? "heading" : "fixed"; SaveFlagPreferences(); await JS.InvokeVoidAsync("lmeMap.setCompassOptions", ShowCompass, CompassMode); await ConfigureCompassSensorAsync(); @@ -2988,7 +2988,7 @@ public bool DrawToolsVisible { get; init; } public bool GpsPositionVisible { get; init; } = true; public bool CompassVisible { get; init; } - public string CompassMode { get; init; } = "heading"; + public string CompassMode { get; init; } = "fixed"; public bool SelectedParcelsVisible { get; init; } = true; public bool TrailEnabled { get; init; } } diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index 237af3a..21339fc 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -29,8 +29,8 @@ it.liviomerola.lmemappecatastali.pro - 1.0.15 - 115 + 1.0.16 + 116 $(ApplicationDisplayVersion) diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index 3e17b55..5651f42 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -1676,43 +1676,77 @@ label { .map-compass { --compass-rotation: 0deg; + --compass-radius: -31px; position: absolute; top: 50%; left: 12px; z-index: 810; - width: 54px; - height: 54px; + width: 82px; + height: 82px; transform: translateY(-50%); border: 2px solid rgba(255, 255, 255, 0.92); border-radius: 50%; background: rgba(22, 31, 38, 0.86); box-shadow: 0 2px 9px rgba(0, 0, 0, 0.3); color: #fff; - pointer-events: none; + cursor: grab; + pointer-events: auto; + touch-action: none; + user-select: none; } -.map-compass-north { +.map-compass.is-dragging { + cursor: grabbing; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.42); +} + +.map-compass-rose { position: absolute; - top: 2px; + inset: 0; + transform: rotate(var(--compass-rotation)); + transition: transform 120ms linear; +} + +.map-compass-direction { + position: absolute; + top: 50%; left: 50%; - transform: translateX(-50%); + width: 20px; + height: 12px; + transform: translate(-50%, -50%) rotate(var(--direction-angle)) translateY(var(--compass-radius)); + color: #f8fafc; + font-size: 9px; + font-weight: 800; + line-height: 1; + text-align: center; +} + +.map-compass-direction > span { + display: block; + transform: rotate(var(--label-counter)); +} + +.map-compass-direction.is-intermediate { + color: #cbd5e1; + font-size: 7px; +} + +.map-compass-direction.is-north { color: #fca5a5; font-size: 11px; font-weight: 900; - line-height: 1; } .map-compass-needle { position: absolute; - bottom: 25px; + bottom: 50%; left: 50%; width: 4px; - height: 18px; - transform: translateX(-50%) rotate(var(--compass-rotation)); + height: 23px; + transform: translateX(-50%); transform-origin: 50% 100%; border-radius: 4px 4px 1px 1px; background: #ef4444; - transition: transform 120ms linear; } .map-compass-needle::after { @@ -2015,9 +2049,10 @@ footer { } .map-compass { + --compass-radius: -27px; left: 8px; - width: 48px; - height: 48px; + width: 72px; + height: 72px; } .map-info-text { diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index 5d0dd59..b5d7aa4 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -65,8 +65,11 @@ const lmeState = { compassLayer: null, compassElement: null, compassVisible: false, - compassMode: "heading", + compassMode: "fixed", compassHeading: 0, + compassPosition: null, + compassDragCleanup: null, + compassResizeHandler: null, infoTimeoutSeconds: 3, infoTimer: null, fileImportBound: false, @@ -321,7 +324,7 @@ function init(dotNetReference) { restoreLayerOrder(); Object.values(managedLayerPanes()).forEach(paneName => { if (!lmeState.map.getPane(paneName)) { - const noRotation = paneName === "gpsPositionPane" || paneName === "fiducialsPane"; + const noRotation = paneName === "gpsPositionPane"; lmeState.map.createPane(paneName, noRotation ? (lmeState.map._norotatePane || lmeState.map._mapPane) : (lmeState.map._rotatePane || lmeState.map._mapPane)); @@ -442,22 +445,150 @@ function createCompassLayer() { const element = L.DomUtil.create("div", "map-compass"); element.setAttribute("role", "img"); element.setAttribute("aria-label", "Bussola: nord"); - element.innerHTML = `N`; + element.setAttribute("title", "Trascina per spostare la bussola"); + element.innerHTML = ``; L.DomEvent.disableClickPropagation(element); + L.DomEvent.disableScrollPropagation(element); map.getContainer().appendChild(element); lmeState.compassElement = element; + restoreCompassPosition(); + bindCompassDragging(element); + lmeState.compassResizeHandler = () => applyCompassPosition(); + map.on("resize", lmeState.compassResizeHandler); + requestAnimationFrame(() => applyCompassPosition()); updateCompassDisplay(); }, - onRemove() { + onRemove(map) { + lmeState.compassDragCleanup?.(); + lmeState.compassDragCleanup = null; + if (lmeState.compassResizeHandler) { + map.off("resize", lmeState.compassResizeHandler); + lmeState.compassResizeHandler = null; + } lmeState.compassElement?.remove(); lmeState.compassElement = null; } }))(); } -function setCompassOptions(visible, mode = "heading") { +function restoreCompassPosition() { + if (lmeState.compassPosition) return; + try { + const saved = JSON.parse(localStorage.getItem("lmeCompassPosition") || "null"); + const x = Number(saved?.x); + const y = Number(saved?.y); + if (Number.isFinite(x) && Number.isFinite(y)) { + lmeState.compassPosition = { + x: Math.max(0, Math.min(1, x)), + y: Math.max(0, Math.min(1, y)) + }; + } + } catch { + } +} + +function applyCompassPosition() { + const element = lmeState.compassElement; + const container = lmeState.map?.getContainer(); + if (!element || !container) return; + const width = container.clientWidth; + const height = container.clientHeight; + const elementWidth = element.offsetWidth; + const elementHeight = element.offsetHeight; + if (!width || !height || !elementWidth || !elementHeight) return; + + const halfWidth = elementWidth / 2; + const halfHeight = elementHeight / 2; + const position = lmeState.compassPosition || { + x: (12 + halfWidth) / width, + y: 0.5 + }; + const centerX = Math.max(halfWidth + 4, Math.min(width - halfWidth - 4, position.x * width)); + const centerY = Math.max(halfHeight + 4, Math.min(height - halfHeight - 4, position.y * height)); + element.style.left = `${centerX - halfWidth}px`; + element.style.top = `${centerY - halfHeight}px`; + element.style.transform = "none"; +} + +function bindCompassDragging(element) { + let pointerId = null; + let offsetX = 0; + let offsetY = 0; + + const move = event => { + if (event.pointerId !== pointerId) return; + event.preventDefault(); + event.stopPropagation(); + const container = lmeState.map?.getContainer(); + if (!container) return; + const containerRect = container.getBoundingClientRect(); + const elementWidth = element.offsetWidth; + const elementHeight = element.offsetHeight; + const left = event.clientX - containerRect.left - offsetX; + const top = event.clientY - containerRect.top - offsetY; + const centerX = Math.max(elementWidth / 2 + 4, + Math.min(containerRect.width - elementWidth / 2 - 4, left + elementWidth / 2)); + const centerY = Math.max(elementHeight / 2 + 4, + Math.min(containerRect.height - elementHeight / 2 - 4, top + elementHeight / 2)); + lmeState.compassPosition = { + x: centerX / containerRect.width, + y: centerY / containerRect.height + }; + applyCompassPosition(); + }; + + const finish = event => { + if (event.pointerId !== pointerId) return; + event.preventDefault(); + event.stopPropagation(); + element.classList.remove("is-dragging"); + if (element.hasPointerCapture?.(pointerId)) { + element.releasePointerCapture(pointerId); + } + pointerId = null; + try { + localStorage.setItem("lmeCompassPosition", JSON.stringify(lmeState.compassPosition)); + } catch { + } + }; + + const start = event => { + if (event.pointerType === "mouse" && event.button !== 0) return; + event.preventDefault(); + event.stopPropagation(); + const rect = element.getBoundingClientRect(); + pointerId = event.pointerId; + offsetX = event.clientX - rect.left; + offsetY = event.clientY - rect.top; + element.classList.add("is-dragging"); + element.setPointerCapture?.(pointerId); + }; + + element.addEventListener("pointerdown", start); + element.addEventListener("pointermove", move); + element.addEventListener("pointerup", finish); + element.addEventListener("pointercancel", finish); + lmeState.compassDragCleanup = () => { + element.removeEventListener("pointerdown", start); + element.removeEventListener("pointermove", move); + element.removeEventListener("pointerup", finish); + element.removeEventListener("pointercancel", finish); + }; +} + +function setCompassOptions(visible, mode = "fixed") { lmeState.compassVisible = Boolean(visible); - lmeState.compassMode = mode === "fixed" ? "fixed" : "heading"; + lmeState.compassMode = mode === "heading" ? "heading" : "fixed"; if (!lmeState.map || !lmeState.compassLayer) return; if (lmeState.compassVisible) { lmeState.managerHiddenCategories.delete("compass"); @@ -5052,10 +5183,14 @@ function loadProjectSnapshot(raw, projectName) { applyStyleFromProperties(layer); } }).addTo(lmeState.selectedParcelsLayer); - L.geoJSON(data.fiducials || emptyFeatureCollection(), { + const restoredFiducials = L.geoJSON(data.fiducials || emptyFeatureCollection(), { pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng), - onEachFeature: (feature, layer) => bindFiducialPopup(feature, layer) - }).addTo(lmeState.fiducials); + onEachFeature: (feature, layer) => { + bindFiducialPopup(feature, layer); + bindManagedMapSelection(layer, "fiducials", lmeState.fiducials); + } + }); + restoredFiducials.eachLayer(layer => lmeState.fiducials.addLayer(layer)); if (lmeState.importedSheetsVisible) { void ensureImportedSheetOutlines().then(() => { applyImportedSheetVisibility(); @@ -6003,7 +6138,9 @@ function setFiducialPointsVisible(enabled) { function setFiducialColor(color) { if (!/^#[0-9a-f]{6}$/i.test(String(color))) return; lmeState.fiducialColor = color; - lmeState.fiducials?.eachLayer(layer => { + const category = layerManagerCategories().find(item => item.key === "fiducials"); + managedRecordsForCategory(category).forEach(record => { + const layer = record.layer; if (layer.feature?.properties) { layer.feature.properties._lmeColor = color; layer.feature.properties._lmeFillColor = color;