Release 0.4.5 with metadata tools and responsive layers
This commit is contained in:
1 parent
00053e4d5f
commit
f480ce23ca
8 files changed
+253
-17
No files matched your search
+100
-5
@@ -618,10 +618,10 @@ input[type="checkbox"] {
|
||||
.layer-manager-block > summary,
|
||||
.layer-manager-object > summary {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 28px minmax(0, 1fr) 50px;
|
||||
grid-template-columns: 28px 28px minmax(0, 1fr) minmax(28px, auto);
|
||||
grid-template-areas:
|
||||
"handle expand title count"
|
||||
". . visibility moves"
|
||||
"visibility lock moves moves"
|
||||
"actions actions actions actions";
|
||||
align-items: start;
|
||||
gap: 6px;
|
||||
@@ -633,7 +633,6 @@ input[type="checkbox"] {
|
||||
|
||||
.layer-expand-button {
|
||||
grid-area: expand;
|
||||
grid-row: 1 / span 3;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
width: 28px;
|
||||
@@ -733,6 +732,7 @@ input[type="checkbox"] {
|
||||
grid-area: moves;
|
||||
display: inline-grid;
|
||||
grid-template-columns: 24px 24px;
|
||||
justify-self: end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@@ -778,12 +778,12 @@ input[type="checkbox"] {
|
||||
}
|
||||
|
||||
.layer-inline-lock {
|
||||
grid-area: visibility;
|
||||
grid-area: lock;
|
||||
justify-self: center;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 24px;
|
||||
margin-left: 26px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
@@ -1047,6 +1047,49 @@ input[type="checkbox"] {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attribute-copy-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(170px, 1fr) auto;
|
||||
grid-template-areas:
|
||||
"heading heading"
|
||||
"source source"
|
||||
"toggle action"
|
||||
"hint hint";
|
||||
align-items: end;
|
||||
gap: 8px 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #f5f8f9;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > strong {
|
||||
grid-area: heading;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > label:first-child {
|
||||
grid-area: source;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > .toggle-line {
|
||||
grid-area: toggle;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > button {
|
||||
grid-area: action;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > label:first-child select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attribute-copy-panel > small {
|
||||
grid-area: hint;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.attribute-editor-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 0.8fr) minmax(180px, 2fr) 36px;
|
||||
@@ -1111,6 +1154,47 @@ input[type="checkbox"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.leaflet-help {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.leaflet-help > p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.leaflet-help-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.leaflet-help-list > div {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.leaflet-help-list dt {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaflet-help-list dd {
|
||||
margin: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.leaflet-help-list dd strong {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.gps-overlay {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
@@ -1661,6 +1745,17 @@ footer {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.attribute-copy-panel {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"heading"
|
||||
"source"
|
||||
"toggle"
|
||||
"action"
|
||||
"hint";
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gps-overlay {
|
||||
top: 68px;
|
||||
right: 8px;
|
||||
|
||||
+75
-7
@@ -132,6 +132,7 @@ window.lmeMap = {
|
||||
setProjectReadOnly,
|
||||
restoreAutosave,
|
||||
compareProjectSnapshots,
|
||||
promptProjectName,
|
||||
setAdvancedOptions,
|
||||
createMapExportGeoJson,
|
||||
createProjectSnapshot,
|
||||
@@ -2277,13 +2278,25 @@ function managedHslToRgb(hue, saturation, lightness) {
|
||||
return values.map(channel => Math.round((channel + offset) * 255));
|
||||
}
|
||||
|
||||
function editableManagedMetadataEntries(record) {
|
||||
const properties = record?.layer?.feature?.properties || {};
|
||||
const reserved = new Set(["name", "Name", "label", "LABEL"]);
|
||||
return Object.entries(properties)
|
||||
.filter(([key, value]) => !key.startsWith("_lme") && !reserved.has(key) && ["string", "number", "boolean"].includes(typeof value));
|
||||
}
|
||||
|
||||
function editManagedAttributes(record) {
|
||||
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
|
||||
const properties = record.layer?.feature?.properties || {};
|
||||
const reserved = new Set(["name", "Name", "label", "LABEL"]);
|
||||
const entries = Object.entries(properties)
|
||||
.filter(([key, value]) => !key.startsWith("_lme") && !reserved.has(key) && ["string", "number", "boolean"].includes(typeof value));
|
||||
const dialog = showAppDialog("Attributi oggetto", `<div class="attribute-editor"><p>Ogni attributo viene salvato nel progetto e negli export GeoJSON, KML e KMZ.</p><div class="attribute-editor-rows"></div><button type="button" data-add-attribute>+ Aggiungi attributo</button><div class="dialog-command-row"><button type="button" data-cancel>Annulla</button><button type="button" class="primary-command" data-save-attributes>Salva attributi</button></div></div>`);
|
||||
const entries = editableManagedMetadataEntries(record);
|
||||
const sourceRecords = layerManagerCategories()
|
||||
.flatMap(category => managedRecordsForCategory(category))
|
||||
.filter(item => item.id !== record.id);
|
||||
const sourceOptions = sourceRecords.map((item, index) =>
|
||||
`<option value="${escapeHtml(item.id)}">${escapeHtml(managedObjectLabel(item, index))} - ${escapeHtml(item.source)}</option>`).join("");
|
||||
const dialog = showAppDialog("Metadati oggetto", `<div class="attribute-editor"><p>Ogni metadato viene salvato nel progetto e negli export GeoJSON, KML e KMZ.</p>
|
||||
<section class="attribute-copy-panel"><strong>Copia da un altro oggetto</strong><label>Oggetto sorgente<select data-copy-source ${sourceRecords.length ? "" : "disabled"}><option value="">Seleziona un oggetto</option>${sourceOptions}</select></label><label class="toggle-line"><input type="checkbox" data-copy-values checked><span>Importa anche i valori</span></label><small>Disattiva il flag per importare soltanto i nomi dei metadati con valore vuoto.</small><button type="button" data-copy-attributes ${sourceRecords.length ? "" : "disabled"}>Copia da</button></section>
|
||||
<div class="attribute-editor-rows"></div><button type="button" data-add-attribute>+ Aggiungi metadato</button><div class="dialog-command-row"><button type="button" data-cancel>Annulla</button><button type="button" class="primary-command" data-save-attributes>Salva metadati</button></div></div>`);
|
||||
const rows = dialog.querySelector(".attribute-editor-rows");
|
||||
const addRow = (key = "", value = "") => {
|
||||
const row = document.createElement("div");
|
||||
@@ -2294,6 +2307,25 @@ function editManagedAttributes(record) {
|
||||
};
|
||||
entries.forEach(([key, value]) => addRow(key, value));
|
||||
if (!entries.length) addRow();
|
||||
const sourceById = new Map(sourceRecords.map(item => [item.id, item]));
|
||||
dialog.querySelector("[data-copy-attributes]")?.addEventListener("click", () => {
|
||||
const source = sourceById.get(dialog.querySelector("[data-copy-source]").value);
|
||||
if (!source) { setInfo("Seleziona l'oggetto da cui copiare i metadati."); return; }
|
||||
const includeValues = dialog.querySelector("[data-copy-values]").checked;
|
||||
const existingRows = new Map([...rows.querySelectorAll(".attribute-editor-row")]
|
||||
.map(row => [row.querySelector("input").value.trim().toLocaleLowerCase("it"), row])
|
||||
.filter(([key]) => key));
|
||||
const sourceEntries = editableManagedMetadataEntries(source);
|
||||
sourceEntries.forEach(([key, value]) => {
|
||||
const existing = existingRows.get(key.toLocaleLowerCase("it"));
|
||||
if (existing) {
|
||||
if (includeValues) existing.querySelector("textarea").value = String(value);
|
||||
} else {
|
||||
addRow(key, includeValues ? value : "");
|
||||
}
|
||||
});
|
||||
setInfo(`${sourceEntries.length} metadati copiati${includeValues ? " con i valori" : " senza valori"}.`);
|
||||
});
|
||||
dialog.querySelector("[data-add-attribute]").addEventListener("click", () => addRow());
|
||||
dialog.querySelector("[data-cancel]").addEventListener("click", () => dialog.remove());
|
||||
dialog.querySelector("[data-save-attributes]").addEventListener("click", () => {
|
||||
@@ -2399,6 +2431,16 @@ function showFormDialog(title, fields, submitLabel = "Salva") {
|
||||
});
|
||||
}
|
||||
|
||||
async function promptProjectName(currentName) {
|
||||
const result = await showFormDialog("Rinomina progetto", [{
|
||||
name: "name",
|
||||
label: "Nuovo nome",
|
||||
value: currentName || "",
|
||||
maxLength: 80
|
||||
}], "Rinomina");
|
||||
return result?.name?.trim() || null;
|
||||
}
|
||||
|
||||
function showObjectTable() {
|
||||
const records = layerManagerCategories().flatMap(category => managedRecordsForCategory(category));
|
||||
lmeState.managerRecords = new Map(records.map(record => [record.id, record]));
|
||||
@@ -2407,9 +2449,9 @@ function showObjectTable() {
|
||||
const details = managedObjectDetails(record.layer);
|
||||
const selected = lmeState.managerSelectedObjectIds.has(record.id);
|
||||
const locked = Boolean(properties._lmeLocked);
|
||||
return `<tr data-object-id="${escapeHtml(record.id)}"><td><input type="checkbox" data-select="${escapeHtml(record.id)}" ${selected ? "checked" : ""} aria-label="Seleziona ${escapeHtml(managedObjectLabel(record, index))}"></td><td><input type="checkbox" data-lock="${escapeHtml(record.id)}" ${locked ? "checked" : ""} ${lmeState.readOnly ? "disabled" : ""} aria-label="Blocca ${escapeHtml(managedObjectLabel(record, index))}"></td><td><button type="button" data-locate="${escapeHtml(record.id)}" title="Mostra sulla mappa" aria-label="Mostra sulla mappa">⌖</button></td><td>${escapeHtml(managedObjectLabel(record, index))}</td><td>${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}</td><td>${escapeHtml(properties.foglio || properties.FOGLIO || "")}</td><td>${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}</td><td>${escapeHtml(details.slice(0, 2).join(" | "))}</td></tr>`;
|
||||
return `<tr data-object-id="${escapeHtml(record.id)}"><td><input type="checkbox" data-select="${escapeHtml(record.id)}" ${selected ? "checked" : ""} aria-label="Seleziona ${escapeHtml(managedObjectLabel(record, index))}"></td><td><input type="checkbox" data-lock="${escapeHtml(record.id)}" ${locked ? "checked" : ""} ${lmeState.readOnly ? "disabled" : ""} aria-label="Blocca ${escapeHtml(managedObjectLabel(record, index))}"></td><td><button type="button" data-locate="${escapeHtml(record.id)}" title="Mostra sulla mappa" aria-label="Mostra sulla mappa">⌖</button></td><td><button type="button" data-attributes="${escapeHtml(record.id)}" title="Modifica metadati" aria-label="Modifica metadati di ${escapeHtml(managedObjectLabel(record, index))}" ${lmeState.readOnly ? "disabled" : ""}>✎</button></td><td>${escapeHtml(managedObjectLabel(record, index))}</td><td>${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}</td><td>${escapeHtml(properties.foglio || properties.FOGLIO || "")}</td><td>${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}</td><td>${escapeHtml(details.slice(0, 2).join(" | "))}</td></tr>`;
|
||||
}).join("");
|
||||
const dialog = showAppDialog("Elenco oggetti e particelle", `<label class="object-table-search">Filtra<input type="search" placeholder="Nome, foglio, particella o gruppo"></label><div class="object-table-legend"><span>Seleziona più oggetti con i flag nella colonna Selez.</span><span>Il lucchetto impedisce modifiche e rimozioni.</span></div><div class="object-table-wrap"><table><thead><tr><th>Selez.</th><th>Blocca</th><th>Mappa</th><th>Nome</th><th>Gruppo</th><th>Foglio</th><th>Particella</th><th>Misure</th></tr></thead><tbody>${body || "<tr><td colspan='8'>Nessun oggetto</td></tr>"}</tbody></table></div>`);
|
||||
const dialog = showAppDialog("Elenco oggetti e particelle", `<label class="object-table-search">Filtra<input type="search" placeholder="Nome, foglio, particella o gruppo"></label><div class="object-table-legend"><span>Seleziona più oggetti con i flag nella colonna Selez.</span><span>Il lucchetto impedisce modifiche e rimozioni; la matita modifica i metadati.</span></div><div class="object-table-wrap"><table><thead><tr><th>Selez.</th><th>Blocca</th><th>Mappa</th><th>Metadati</th><th>Nome</th><th>Gruppo</th><th>Foglio</th><th>Particella</th><th>Misure</th></tr></thead><tbody>${body || "<tr><td colspan='9'>Nessun oggetto</td></tr>"}</tbody></table></div>`);
|
||||
const byId = new Map(records.map(record => [record.id, record]));
|
||||
dialog.querySelector("input[type='search']").addEventListener("input", event => {
|
||||
const query = event.target.value.toLocaleLowerCase("it");
|
||||
@@ -2437,6 +2479,10 @@ function showObjectTable() {
|
||||
const record = byId.get(button.dataset.locate); if (!record) return;
|
||||
dialog.remove(); highlightManagedObject(record);
|
||||
}));
|
||||
dialog.querySelectorAll("[data-attributes]").forEach(button => button.addEventListener("click", () => {
|
||||
const record = byId.get(button.dataset.attributes); if (!record) return;
|
||||
dialog.remove(); editManagedAttributes(record);
|
||||
}));
|
||||
return records.length;
|
||||
}
|
||||
|
||||
@@ -2729,6 +2775,27 @@ function showGeometryResult(title, message, tone = "success") {
|
||||
showAppDialog(title, `<div class="geometry-result geometry-result-${tone}"><span aria-hidden="true">${icons[tone] || "i"}</span><p>${escapeHtml(message)}</p></div>`);
|
||||
}
|
||||
|
||||
function showLeafletToolbarHelp() {
|
||||
showAppDialog("Guida strumenti mappa", `<div class="leaflet-help">
|
||||
<p>Gli strumenti operano direttamente sulla mappa. Per le azioni su oggetti esistenti, seleziona prima le righe nel pannello Layer, nella tabella oggetti oppure tocca un oggetto sulla mappa.</p>
|
||||
<dl class="leaflet-help-list">
|
||||
<div><dt>+</dt><dd><strong>Ingrandisci</strong>Aumenta lo zoom mantenendo il centro attuale. Esempio: avvicinati a un vertice per posizionarlo con maggiore precisione.</dd></div>
|
||||
<div><dt>−</dt><dd><strong>Riduci</strong>Allarga l'area visibile. Esempio: usalo per controllare insieme particelle appartenenti a fogli vicini.</dd></div>
|
||||
<div><dt>●</dt><dd><strong>Punto di interesse</strong>Tocca la posizione da memorizzare. Esempio: segna un cancello, un cippo o il punto di accesso al fondo.</dd></div>
|
||||
<div><dt>/</dt><dd><strong>Linea</strong>Tocca i vertici del percorso e termina con doppio tocco o sul primo comando della barra. Durante il disegno viene mostrata la distanza; utile per un fronte strada o una recinzione.</dd></div>
|
||||
<div><dt>⬠</dt><dd><strong>Poligono</strong>Tocca almeno tre vertici e chiudi la figura. Esempio: delimita un'area di intervento; l'app calcola area e perimetro.</dd></div>
|
||||
<div><dt>□</dt><dd><strong>Rettangolo</strong>Trascina tra due angoli opposti. Esempio: crea rapidamente l'ingombro rettangolare di un fabbricato.</dd></div>
|
||||
<div><dt>○</dt><dd><strong>Cerchio</strong>Tocca il centro e trascina fino al raggio desiderato. Esempio: rappresenta una fascia di rispetto attorno a un pozzo.</dd></div>
|
||||
<div><dt>✎</dt><dd><strong>Modifica disegni</strong>Premi la matita e scegli Solo selezionato oppure Tutti. Sposta i vertici e conferma con Salva; gli oggetti bloccati devono prima essere sbloccati.</dd></div>
|
||||
<div><dt>▱</dt><dd><strong>Elimina disegni</strong>Premi il cestino e scegli Solo selezionato oppure Tutti. L'oggetto rimosso può essere recuperato con Annulla o dal cestino negli strumenti avanzati.</dd></div>
|
||||
<div><dt>✓</dt><dd><strong>Controlla e ripara</strong>Verifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.</dd></div>
|
||||
<div><dt>◉</dt><dd><strong>Crea foro</strong>Seleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.</dd></div>
|
||||
<div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Seleziona esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde, mostrando le rispettive aree.</dd></div>
|
||||
<div><dt>?</dt><dd><strong>Guida</strong>Riapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra, così l'operazione non resta senza riscontro.</dd></div>
|
||||
</dl>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function addGeometryToolsControl() {
|
||||
if (lmeState.geometryToolsControl || !lmeState.map) return;
|
||||
const GeometryTools = L.Control.extend({
|
||||
@@ -2738,7 +2805,8 @@ function addGeometryToolsControl() {
|
||||
const actions = [
|
||||
{ icon: "✓", title: "Controlla e ripara geometrie", run: validateAndRepairSelected },
|
||||
{ icon: "◉", title: "Crea foro da due poligoni", run: createHoleFromSelected },
|
||||
{ icon: "⇄", title: "Confronta due confini", run: compareSelectedGeometries }
|
||||
{ icon: "⇄", title: "Confronta due confini", run: compareSelectedGeometries },
|
||||
{ icon: "?", title: "Guida strumenti mappa", run: showLeafletToolbarHelp }
|
||||
];
|
||||
actions.forEach(action => {
|
||||
const button = L.DomUtil.create("a", "", container);
|
||||
|
||||
Reference in new issue
Block a user