Release 0.4.3 with contextual help and parcel deduplication
This commit is contained in:
1 parent
f1224b21c4
commit
cfba1b998b
8 files changed
+325
-11
No files matched your search
@@ -306,6 +306,73 @@ input[type="checkbox"] {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.menu-help {
|
||||
margin-top: 4px;
|
||||
border: 1px solid #c8d3d8;
|
||||
border-radius: 6px;
|
||||
background: #f7fafb;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.menu-help summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-help summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-help summary > span {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #82949d;
|
||||
border-radius: 50%;
|
||||
color: #42535c;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.menu-help summary::after {
|
||||
margin-left: auto;
|
||||
content: "\25BE";
|
||||
color: #52636b;
|
||||
font-size: 14px;
|
||||
transition: transform 140ms ease;
|
||||
}
|
||||
|
||||
.menu-help[open] summary::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu-help-content {
|
||||
padding: 10px 12px 12px;
|
||||
border-top: 1px solid #d8e0e4;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.menu-help-content p {
|
||||
margin: 0 0 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menu-help-content ol {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu-help-content li + li {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.layer-tree-section {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
+116
-5
@@ -367,7 +367,7 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
|
||||
const lon = Number(longitude);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
setInfo("Coordinate della particella non valide.");
|
||||
return;
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -389,27 +389,63 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
|
||||
lmeState.map.setView([lat, lon], 20);
|
||||
}
|
||||
setInfo("Posizione trovata, ma il servizio WFS dell'Agenzia non ha restituito il confine. Riprova tra poco.");
|
||||
return;
|
||||
return "missing";
|
||||
}
|
||||
|
||||
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || label;
|
||||
const targetGroup = normalizedParcel ? lmeState.selectedParcelsLayer : lmeState.gpsParcelLayer;
|
||||
if (normalizedParcel) {
|
||||
lmeState.selectedParcelsVisible = true;
|
||||
lmeState.managerHiddenCategories.delete("selectedParcels");
|
||||
if (!lmeState.map.hasLayer(targetGroup)) {
|
||||
targetGroup.addTo(lmeState.map);
|
||||
}
|
||||
targetGroup.bringToFront();
|
||||
}
|
||||
if (!normalizedParcel) {
|
||||
targetGroup.clearLayers();
|
||||
}
|
||||
|
||||
const existingLayer = normalizedParcel
|
||||
? findParcelLayerByReference(targetGroup, ref)
|
||||
? findParcelLayerByReference(targetGroup, ref) || restoreHiddenParcelLayerByReference(ref)
|
||||
: null;
|
||||
if (existingLayer) {
|
||||
if (typeof existingLayer.setStyle === "function") {
|
||||
existingLayer.setStyle({ color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 });
|
||||
}
|
||||
targetGroup.bringToFront();
|
||||
if (shouldFit) {
|
||||
fitLayer(existingLayer);
|
||||
}
|
||||
setText("parcelState", `Particella selezionata: ${ref}`);
|
||||
setInfo(`La particella ${ref} e' gia evidenziata sulla mappa.`);
|
||||
return;
|
||||
updateGpsInsideState();
|
||||
refreshLayerManager();
|
||||
return "selected";
|
||||
}
|
||||
|
||||
const importedRecord = normalizedParcel ? findImportedParcelRecord(feature, ref) : null;
|
||||
if (importedRecord) {
|
||||
restoreManagedRecordVisibility(importedRecord);
|
||||
lmeState.importedVisible = true;
|
||||
lmeState.managerHiddenCategories.delete("imported");
|
||||
if (!lmeState.map.hasLayer(lmeState.imported)) {
|
||||
lmeState.imported.addTo(lmeState.map);
|
||||
}
|
||||
lmeState.imported.bringToFront();
|
||||
if (shouldFit) {
|
||||
fitLayer(importedRecord.layer);
|
||||
}
|
||||
setText("parcelState", `Particella gia presente nei dati importati: ${ref}`);
|
||||
setInfo(`La particella ${ref} e' gia presente in un progetto o file importato: nessun duplicato aggiunto.`);
|
||||
refreshLayerManager();
|
||||
return "imported";
|
||||
}
|
||||
|
||||
feature.properties = {
|
||||
...(feature.properties || {}),
|
||||
_lmeParcelReference: String(ref || label || normalizedParcel)
|
||||
};
|
||||
const layer = L.geoJSON(feature, {
|
||||
pane: normalizedParcel ? "selectedParcelsPane" : "gpsParcelsPane",
|
||||
style: normalizedParcel
|
||||
@@ -418,6 +454,9 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
|
||||
}).addTo(targetGroup);
|
||||
layer.lmeParcelReference = ref;
|
||||
layer.bindPopup(`<strong>${escapeHtml(ref)}</strong>`);
|
||||
if (normalizedParcel) {
|
||||
targetGroup.bringToFront();
|
||||
}
|
||||
if (shouldFit) {
|
||||
fitLayer(layer);
|
||||
}
|
||||
@@ -429,9 +468,12 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
|
||||
} else {
|
||||
setText("parcelState", `Dentro particella AdE: ${ref}`);
|
||||
}
|
||||
updateGpsInsideState();
|
||||
refreshLayerManager();
|
||||
return normalizedParcel ? "added" : "gps";
|
||||
} catch (error) {
|
||||
setInfo(`Geometria catastale non leggibile: ${error.message}`);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,12 +482,13 @@ function normalizeParcel(parcel) {
|
||||
}
|
||||
|
||||
function findParcelLayerByReference(group, reference) {
|
||||
const expected = normalizeParcelReference(reference);
|
||||
let match = null;
|
||||
group.eachLayer(layer => {
|
||||
if (match) {
|
||||
return;
|
||||
}
|
||||
if (layer.lmeParcelReference === reference) {
|
||||
if (parcelLayerMatchesReference(layer, expected)) {
|
||||
match = layer;
|
||||
return;
|
||||
}
|
||||
@@ -456,6 +499,74 @@ function findParcelLayerByReference(group, reference) {
|
||||
return match;
|
||||
}
|
||||
|
||||
function restoreHiddenParcelLayerByReference(reference) {
|
||||
const expected = normalizeParcelReference(reference);
|
||||
const entry = [...lmeState.managerHiddenObjects.entries()].find(([, record]) =>
|
||||
record.categoryKey === "selectedParcels" && parcelLayerMatchesReference(record.layer, expected));
|
||||
if (!entry) return null;
|
||||
const [id, record] = entry;
|
||||
lmeState.managerHiddenObjects.delete(id);
|
||||
lmeState.managerHiddenObjectIds.delete(id);
|
||||
lmeState.managerHiddenMeasurementSummaryIds.delete(id);
|
||||
lmeState.managerHiddenMeasurementSideIds.delete(id);
|
||||
record.parent?.addLayer?.(record.layer);
|
||||
return record.layer;
|
||||
}
|
||||
|
||||
function parcelLayerMatchesReference(layer, normalizedReference) {
|
||||
const properties = layer?.feature?.properties || {};
|
||||
return [
|
||||
layer?.lmeParcelReference,
|
||||
properties._lmeParcelReference,
|
||||
properties.NATIONALCADASTRALREFERENCE,
|
||||
properties.INSPIREID_LOCALID
|
||||
].some(value => normalizeParcelReference(value) === normalizedReference);
|
||||
}
|
||||
|
||||
function findImportedParcelRecord(feature, reference) {
|
||||
const category = layerManagerCategories().find(item => item.key === "imported");
|
||||
if (!category) return null;
|
||||
const normalizedReference = normalizeParcelReference(reference);
|
||||
return managedRecordsForCategory(category).find(record =>
|
||||
parcelLayerMatchesReference(record.layer, normalizedReference)
|
||||
|| parcelGeometriesEquivalent(record.layer?.toGeoJSON?.(), feature)) || null;
|
||||
}
|
||||
|
||||
function restoreManagedRecordVisibility(record) {
|
||||
if (!record) return;
|
||||
lmeState.managerHiddenObjects.delete(record.id);
|
||||
lmeState.managerHiddenObjectIds.delete(record.id);
|
||||
lmeState.managerHiddenMeasurementSummaryIds.delete(record.id);
|
||||
lmeState.managerHiddenMeasurementSideIds.delete(record.id);
|
||||
if (record.parent && !record.parent.hasLayer?.(record.layer)) {
|
||||
record.parent.addLayer?.(record.layer);
|
||||
}
|
||||
}
|
||||
|
||||
function parcelGeometriesEquivalent(candidate, target) {
|
||||
const candidateType = candidate?.geometry?.type;
|
||||
const targetType = target?.geometry?.type;
|
||||
if (!["Polygon", "MultiPolygon"].includes(candidateType)
|
||||
|| !["Polygon", "MultiPolygon"].includes(targetType)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const candidateArea = turf.area(candidate);
|
||||
const targetArea = turf.area(target);
|
||||
if (!(candidateArea > 0) || !(targetArea > 0)) return false;
|
||||
const areaRatio = Math.max(candidateArea, targetArea) / Math.min(candidateArea, targetArea);
|
||||
if (areaRatio > 1.03) return false;
|
||||
const overlap = turf.intersect(turf.featureCollection([candidate, target]));
|
||||
return Boolean(overlap) && turf.area(overlap) / Math.min(candidateArea, targetArea) >= 0.985;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeParcelReference(reference) {
|
||||
return String(reference || "").trim().toUpperCase().replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (!lmeState.map) {
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user