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
+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