@@ -28,6 +28,7 @@ const lmeState = {
drawControl : null ,
drawingDeleteButton : null ,
drawingScopeMenu : null ,
geometryToolsControl : null ,
ignoreMapClickUntil : 0 ,
activeDrawHandler : null ,
drawEventsBound : false ,
@@ -80,6 +81,7 @@ const lmeState = {
snapEnabled : true ,
snapDistancePixels : 14 ,
autoSaveEnabled : true ,
autoFitSelectedObject : false ,
gpsProximityEnabled : true ,
gpsBoundaryAlertEnabled : false ,
lastBoundaryState : null ,
@@ -251,6 +253,7 @@ function init(dotNetReference) {
refreshCadastreLayer ( ) ;
bindFileImport ( ) ;
bindDrawingEvents ( ) ;
addGeometryToolsControl ( ) ;
if ( ! lmeState . resizeBound ) {
window . addEventListener ( "resize" , resize ) ;
@@ -1181,12 +1184,12 @@ function drawingLayerLabel(layer, layerType) {
function layerManagerCategories ( ) {
return [
{ key : "gpsPosition" , label : "Posizione GPS" , roots : [ lmeState . gpsMarker , lmeState . gpsAccuracy ] . filter ( Boolean ) } ,
{ key : "drawings" , label : "Disegni e POI" , roots : [ lmeState . drawings ] . filter ( Boolean ) } ,
{ key : "drawings" , label : "Disegni e POI" , roots : [ lmeState . drawings ] . filter ( Boolean ) , blocks : true } ,
{ key : "gpsTrail" , label : "Traccia GPS" , roots : [ lmeState . gpsTrail ] . filter ( Boolean ) } ,
{ key : "gpsParcels" , label : "Particella rilevata dal GPS" , roots : [ lmeState . gpsParcelLayer ] . filter ( Boolean ) } ,
{ key : "selectedParcels" , label : "Particelle catastali evidenziate" , roots : [ lmeState . selectedParcelsLayer ] . filter ( Boolean ) } ,
{ key : "importedSheets" , label : "Cornici e fogli importati" , roots : [ lmeState . importedSheets ] . filter ( Boolean ) , blocks : true } ,
{ key : "imported" , label : "D ati importati" , roots : [ lmeState . imported ] . filter ( Boolean ) , blocks : true } ,
{ key : "imported" , label : "Progetti e d ati importati" , roots : [ lmeState . imported ] . filter ( Boolean ) , blocks : true } ,
{ key : "cadastre" , label : "Layer catastali AdE" , roots : [ lmeState . cadastre ] . filter ( Boolean ) }
] ;
}
@@ -1237,11 +1240,13 @@ function managedRecordsForCategory(category) {
function managedObjectLabel ( record , index ) {
const layer = record . layer ;
const properties = layer ? . feature ? . properties || { } ;
const reference = properties . NATIONALCADASTRALREFERENCE
const reference = properties . _lmeDisplayName
|| properties . name
|| properties . Name
|| properties . NATIONALCADASTRALREFERENCE
|| properties . INSPIREID _LOCALID
|| properties . LABEL
|| properties . name
|| properties . Name ;
|| properties . label ;
if ( reference ) {
return String ( reference ) ;
}
@@ -1265,7 +1270,7 @@ function managedObjectLabel(record, index) {
function managedObjectEditableName ( record ) {
const properties = record ? . layer ? . feature ? . properties || { } ;
return properties . name || properties . Name || properties . label || properties . LABEL || "" ;
return properties . _lmeDisplayName || properties . name || properties . Name || properties . label || properties . LABEL || "" ;
}
function managedObjectCanRename ( record ) {
@@ -1273,16 +1278,15 @@ function managedObjectCanRename(record) {
&& record . layer ? . feature ? . properties ;
}
function renameManagedObject ( record , index ) {
async function renameManagedObject ( record , index ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
if ( ! managedObjectCanRename ( record ) ) {
return ;
}
const currentName = String ( managedObjectEditableName ( record ) || managedObjectLabel ( record , index ) ) . trim ( ) ;
const requestedName = window . prompt ( "Nuovo nome oggetto" , currentName ) ;
if ( requestedName === null ) {
return ;
}
const result = await showFormDialog ( "Rinomina oggetto" , [ { name : "name" , label : "Nome" , value : currentName , maxLength : 120 } ] , "Rinomina" ) ;
if ( ! result ) return ;
const requestedName = result . name ;
const name = requestedName . trim ( ) . slice ( 0 , 120 ) ;
if ( ! name ) {
setInfo ( "Nome non modificato: inserisci un testo." ) ;
@@ -1291,6 +1295,7 @@ function renameManagedObject(record, index) {
rememberMapChange ( ) ;
const properties = record . layer . feature . properties ;
properties . name = name ;
properties . _lmeDisplayName = name ;
record . layer . lmeManagerLabel = record . categoryKey === "drawings" && record . layer instanceof L . Marker
? ` POI: ${ name } `
: null ;
@@ -1319,12 +1324,11 @@ function managedCategoryCanRename(category) {
return [ "drawings" , "imported" , "importedSheets" ] . includes ( category . key ) ;
}
function renameManagedCategory ( category , records ) {
async function renameManagedCategory ( category , records ) {
const currentName = managedCategoryName ( category , records ) ;
const requestedName = window . prompt ( "Nuovo nome gruppo" , currentName ) ;
if ( requestedName === null ) {
return ;
}
const result = await showFormDialog ( "Rinomina gruppo" , [ { name : "name" , label : "Nome gruppo" , value : currentName , maxLength : 120 } ] , "Rinomina" ) ;
if ( ! result ) return ;
const requestedName = result . name ;
const name = requestedName . trim ( ) . slice ( 0 , 120 ) ;
if ( ! name ) {
setInfo ( "Nome non modificato: inserisci un testo." ) ;
@@ -1348,12 +1352,11 @@ function managedBlockName(source, records) {
. find ( Boolean ) || source ) ;
}
function renameManagedBlock ( category , source , records ) {
async function renameManagedBlock ( category , source , records ) {
const currentName = managedBlockName ( source , records ) ;
const requestedName = window . prompt ( "Nuovo nome gruppo" , currentName ) ;
if ( requestedName === null ) {
return ;
}
const result = await showFormDialog ( "Rinomina gruppo" , [ { name : "name" , label : "Nome gruppo" , value : currentName , maxLength : 120 } ] , "Rinomina" ) ;
if ( ! result ) return ;
const requestedName = result . name ;
const name = requestedName . trim ( ) . slice ( 0 , 120 ) ;
if ( ! name ) {
setInfo ( "Nome non modificato: inserisci un testo." ) ;
@@ -1379,11 +1382,11 @@ function managedObjectCanRemove(record) {
return [ "drawings" , "imported" , "importedSheets" , "selectedParcels" , "gpsParcels" ] . includes ( record . categoryKey ) ;
}
function layerManagerRowActions ( renameButton , removeButton ) {
if ( ! renameButton && ! removeButton ) {
function layerManagerRowActions ( renameButton , moveButton , removeButton ) {
if ( ! renameButton && ! moveButton && ! removeButton ) {
return "" ;
}
return ` <span class="layer-row-actions"> ${ renameButton } ${ removeButton } </span> ` ;
return ` <span class="layer-row-actions"> ${ renameButton } ${ moveButton } ${ removeButton } </span> ` ;
}
const layerManagerPalettes = [
@@ -1469,6 +1472,126 @@ function removeManagedRecords(records, subject) {
setInfo ( ` ${ records . length === 1 ? "Oggetto rimosso" : ` ${ records . length } oggetti rimossi ` } dal progetto. ` ) ;
}
function managedMoveDestinations ( ) {
return layerManagerCategories ( )
. filter ( category => [ "drawings" , "imported" ] . includes ( category . key ) )
. map ( category => {
const records = managedRecordsForCategory ( category ) ;
const groups = new Map ( ) ;
records . forEach ( record => {
if ( ! groups . has ( record . source ) ) {
groups . set ( record . source , managedBlockName ( record . source , [ record ] ) ) ;
}
} ) ;
if ( ! groups . size ) {
groups . set ( "Progetto corrente" , "Gruppo principale" ) ;
}
return {
key : category . key ,
name : managedCategoryName ( category , records ) ,
groups : [ ... groups ] . map ( ( [ source , name ] ) => ( { source , name } ) )
} ;
} ) ;
}
function showMoveManagedRecordsDialog ( records , subject ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
if ( ! records . length || records . some ( record => ! [ "drawings" , "imported" ] . includes ( record . categoryKey ) ) ) {
setInfo ( "Questo oggetto o gruppo non puo essere spostato." ) ;
return ;
}
if ( records . some ( record => record . layer ? . feature ? . properties ? . _lmeLocked ) ) {
setInfo ( "Sblocca prima tutti gli oggetti da spostare." ) ;
return ;
}
const destinations = managedMoveDestinations ( ) ;
const categoryOptions = destinations . map ( destination =>
` <option value=" ${ escapeHtml ( destination . key ) } "> ${ escapeHtml ( destination . name ) } </option> ` ) . join ( "" ) ;
const dialog = showAppDialog ( "Sposta nel gruppo" , ` <form class="lme-edit-form move-group-form"><p>Cambia l'appartenenza di ${ escapeHtml ( subject ) } senza modificarne la posizione sulla mappa.</p><label>Categoria<select name="category"> ${ categoryOptions } </select></label><label>Gruppo esistente<select name="group"></select></label><label>Oppure crea un nuovo gruppo<input name="newGroup" type="text" maxlength="120" placeholder="Nome del nuovo gruppo"></label><div class="dialog-command-row"><button type="button" data-cancel>Annulla</button><button type="submit" class="primary-command">Sposta nel gruppo</button></div></form> ` ) ;
const form = dialog . querySelector ( "form" ) ;
const categorySelect = form . elements . category ;
const groupSelect = form . elements . group ;
const newGroupInput = form . elements . newGroup ;
const currentCategory = records . every ( record => record . categoryKey === records [ 0 ] . categoryKey )
? records [ 0 ] . categoryKey
: "drawings" ;
categorySelect . value = currentCategory ;
const refreshGroups = ( ) => {
const destination = destinations . find ( item => item . key === categorySelect . value ) ;
groupSelect . innerHTML = ( destination ? . groups || [ ] ) . map ( group =>
` <option value=" ${ escapeHtml ( group . source ) } "> ${ escapeHtml ( group . name ) } </option> ` ) . join ( "" ) ;
const currentSource = records . every ( record => record . source === records [ 0 ] . source )
? records [ 0 ] . source
: "" ;
if ( destination ? . key === currentCategory && currentSource
&& destination . groups . some ( group => group . source === currentSource ) ) {
groupSelect . value = currentSource ;
}
} ;
categorySelect . addEventListener ( "change" , refreshGroups ) ;
refreshGroups ( ) ;
dialog . querySelector ( "[data-cancel]" ) . addEventListener ( "click" , ( ) => dialog . remove ( ) ) ;
form . addEventListener ( "submit" , event => {
event . preventDefault ( ) ;
const destination = destinations . find ( item => item . key === categorySelect . value ) ;
if ( ! destination ) return ;
const requestedName = newGroupInput . value . trim ( ) . slice ( 0 , 120 ) ;
const existingByName = requestedName
? destination . groups . find ( group => group . name . toLocaleLowerCase ( "it" ) === requestedName . toLocaleLowerCase ( "it" ) )
: null ;
const selectedGroup = destination . groups . find ( group => group . source === groupSelect . value ) ;
const target = existingByName || ( requestedName
? { source : ` Gruppo ${ requestedName } ` , name : requestedName }
: selectedGroup ) ;
if ( ! target ) {
setInfo ( "Scegli un gruppo di destinazione o inserisci il nome di un nuovo gruppo." ) ;
return ;
}
dialog . remove ( ) ;
moveManagedRecordsToGroup ( records , destination . key , target . source , target . name ) ;
} ) ;
}
function moveManagedRecordsToGroup ( records , targetCategory , targetSource , targetGroupName ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
const movable = records . filter ( record => [ "drawings" , "imported" ] . includes ( record . categoryKey ) ) ;
if ( ! movable . length || ! [ "drawings" , "imported" ] . includes ( targetCategory ) ) {
setInfo ( "Questo gruppo non puo essere spostato." ) ;
return ;
}
if ( movable . some ( record => record . layer ? . feature ? . properties ? . _lmeLocked ) ) {
setInfo ( "Sblocca il gruppo prima di spostarlo." ) ;
return ;
}
rememberMapChange ( ) ;
const features = movable . map ( record => {
const feature = record . layer . toGeoJSON ( ) ;
feature . properties = {
... ( feature . properties || { } ) ,
_lmeSource : targetSource ,
_lmeGroupName : targetGroupName
} ;
delete feature . properties . _lmeManagerId ;
return feature ;
} ) ;
movable . forEach ( record => {
record . parent ? . removeLayer ? . ( record . layer ) ;
clearManagedObjectState ( record . id ) ;
lmeState . managerSelectedObjectIds . delete ( record . id ) ;
} ) ;
if ( targetCategory === "drawings" ) {
features . forEach ( feature => addDrawingFeature ( feature ) ) ;
} else {
addImportedFeatures ( { type : "FeatureCollection" , features } ) ;
}
refreshImportedMeasurements ( ) ;
refreshDrawingMeasurements ( ) ;
refreshLayerManager ( ) ;
const categoryName = targetCategory === "drawings" ? "Disegni e POI" : "Progetti e dati importati" ;
finishMapChange ( ` Spostati ${ features . length } oggetti in ${ categoryName } > ${ targetGroupName } . ` ) ;
setInfo ( ` ${ features . length } oggetti spostati in ${ categoryName } > ${ targetGroupName } . ` ) ;
}
function managedObjectDetails ( layer ) {
const details = [ ] ;
const properties = layer ? . feature ? . properties || { } ;
@@ -1546,6 +1669,9 @@ function refreshLayerManager() {
applyManagedVisualOrder ( container ) ;
bindLayerManagerDrag ( container ) ;
bindLayerManagerMoveButtons ( container ) ;
if ( lmeState . managerHighlightedObjectId ) {
window . requestAnimationFrame ( ( ) => revealManagedRecordInLayerManager ( lmeState . managerHighlightedObjectId ) ) ;
}
if ( scrollHost ) {
scrollHost . scrollTop = scrollTop ;
window . requestAnimationFrame ( ( ) => {
@@ -1557,6 +1683,8 @@ function refreshLayerManager() {
function renderLayerManagerCategory ( category , records ) {
const group = document . createElement ( "details" ) ;
group . className = "layer-manager-group" ;
group . classList . toggle ( "layer-manager-selection-path" , records . some ( record =>
record . id === lmeState . managerHighlightedObjectId || lmeState . managerSelectedObjectIds . has ( record . id ) ) ) ;
group . dataset . dragType = "category" ;
group . dataset . layerDepth = "0" ;
group . dataset . categoryKey = category . key ;
@@ -1569,14 +1697,18 @@ function renderLayerManagerCategory(category, records) {
const removeButton = managedCategoryCanRemove ( category , records )
? ` <button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button> `
: "" ;
const rowActions = layerManagerRowActions ( renameButton , removeButton ) ;
const moveButton = records . length && [ "drawings" , "imported" ] . includes ( category . key )
? ` <button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button> `
: "" ;
const rowActions = layerManagerRowActions ( renameButton , moveButton , removeButton ) ;
group . classList . toggle ( "layer-manager-with-actions" , Boolean ( rowActions ) ) ;
const visibleCount = records . filter ( record => ! lmeState . managerHiddenObjectIds . has ( record . id ) ) . length ;
const categoryVisible = ! lmeState . managerHiddenCategories . has ( category . key )
&& ( ! category . roots . length || category . roots . some ( layer => lmeState . map . hasLayer ( layer ) ) ) ;
const visible = categoryVisible ;
const categoryLocked = records . length > 0 && records . every ( record => Boolean ( record . layer ? . feature ? . properties ? . _lmeLocked ) ) ;
group . innerHTML = ` <summary><button type="button" class="layer-drag-handle" aria-label="Trascina gruppo">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra ${ escapeHtml ( categoryName ) } "><span class="layer-manager-title"> ${ escapeHtml ( categoryName ) } </span><button type="button" class="layer-inline-lock ${ categoryLocked ? "is-locked" : "" } " aria-label=" ${ categoryLocked ? "Sblocca" : "Blocca" } gruppo" title=" ${ categoryLocked ? "Sblocca" : "Blocca" } "> ${ categoryLocked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "gruppo" ) } <span class="layer-manager-count"> ${ records . length } </span></summary><div class="layer-manager-children"></div> ` ;
group . innerHTML = ` <summary> ${ layerExpandButton ( group . open , categoryName ) } <button type="button" class="layer-drag-handle" aria-label="Trascina gruppo">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra ${ escapeHtml ( categoryName ) } "><span class="layer-manager-title"> ${ escapeHtml ( categoryName ) } </span><button type="button" class="layer-inline-lock ${ categoryLocked ? "is-locked" : "" } " aria-label=" ${ categoryLocked ? "Sblocca" : "Blocca" } gruppo" title=" ${ categoryLocked ? "Sblocca" : "Blocca" } "> ${ categoryLocked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "gruppo" ) } <span class="layer-manager-count"> ${ records . length } </span></summary><div class="layer-manager-children"></div> ` ;
bindLayerManagerToggleButton ( group ) ;
const checkbox = group . querySelector ( "input" ) ;
checkbox . indeterminate = categoryVisible && records . length > 0 && visibleCount < records . length ;
checkbox . addEventListener ( "click" , event => event . stopPropagation ( ) ) ;
@@ -1595,6 +1727,11 @@ function renderLayerManagerCategory(category, records) {
event . stopPropagation ( ) ;
removeManagedRecords ( records , ` il gruppo ' ${ categoryName } ' ` ) ;
} ) ;
group . querySelector ( ".layer-membership-button" ) ? . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
showMoveManagedRecordsDialog ( records , ` il gruppo ' ${ categoryName } ' ` ) ;
} ) ;
bindManagedParentHighlight ( group , ` category: ${ category . key } ` , records ) ;
const children = group . querySelector ( ".layer-manager-children" ) ;
if ( records . length ) children . appendChild ( renderManagedGroupTools ( records ) ) ;
@@ -1615,6 +1752,8 @@ function renderLayerManagerCategory(category, records) {
function renderLayerManagerBlock ( category , source , records ) {
const block = document . createElement ( "details" ) ;
block . className = "layer-manager-block" ;
block . classList . toggle ( "layer-manager-selection-path" , records . some ( record =>
record . id === lmeState . managerHighlightedObjectId || lmeState . managerSelectedObjectIds . has ( record . id ) ) ) ;
block . dataset . dragType = "block" ;
block . dataset . layerDepth = "1" ;
block . dataset . categoryKey = category . key ;
@@ -1626,10 +1765,12 @@ function renderLayerManagerBlock(category, source, records) {
const blockName = managedBlockName ( source , records ) ;
const rowActions = layerManagerRowActions (
` <button type="button" class="layer-rename-button" aria-label="Rinomina gruppo" title="Rinomina">Rinomina</button> ` ,
` <button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button> ` ,
` <button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button> ` ) ;
block . classList . add ( "layer-manager-with-actions" ) ;
const blockLocked = records . length > 0 && records . every ( record => Boolean ( record . layer ? . feature ? . properties ? . _lmeLocked ) ) ;
block . innerHTML = ` <summary><button type="button" class="layer-drag-handle" aria-label="Trascina blocco">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra ${ escapeHtml ( blockName ) } "><span class="layer-manager-title"> ${ escapeHtml ( blockName ) } </span><button type="button" class="layer-inline-lock ${ blockLocked ? "is-locked" : "" } " aria-label=" ${ blockLocked ? "Sblocca" : "Blocca" } gruppo" title=" ${ blockLocked ? "Sblocca" : "Blocca" } "> ${ blockLocked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "blocco" ) } <span class="layer-manager-count"> ${ records . length } </span></summary><div class="layer-manager-children"></div> ` ;
block . innerHTML = ` <summary> ${ layerExpandButton ( block . open , blockName ) } <button type="button" class="layer-drag-handle" aria-label="Trascina blocco">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra ${ escapeHtml ( blockName ) } "><span class="layer-manager-title"> ${ escapeHtml ( blockName ) } </span><button type="button" class="layer-inline-lock ${ blockLocked ? "is-locked" : "" } " aria-label=" ${ blockLocked ? "Sblocca" : "Blocca" } gruppo" title=" ${ blockLocked ? "Sblocca" : "Blocca" } "> ${ blockLocked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "blocco" ) } <span class="layer-manager-count"> ${ records . length } </span></summary><div class="layer-manager-children"></div> ` ;
bindLayerManagerToggleButton ( block ) ;
const checkbox = block . querySelector ( "input" ) ;
checkbox . indeterminate = visibleCount > 0 && ! visible ;
checkbox . addEventListener ( "click" , event => event . stopPropagation ( ) ) ;
@@ -1651,6 +1792,11 @@ function renderLayerManagerBlock(category, source, records) {
event . stopPropagation ( ) ;
removeManagedRecords ( records , ` il gruppo ' ${ blockName } ' ` ) ;
} ) ;
block . querySelector ( ".layer-membership-button" ) . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
showMoveManagedRecordsDialog ( records , ` il gruppo ' ${ blockName } ' ` ) ;
} ) ;
bindManagedParentHighlight ( block , ` block: ${ category . key } : ${ source } ` , records ) ;
const children = block . querySelector ( ".layer-manager-children" ) ;
children . appendChild ( renderManagedGroupTools ( records ) ) ;
@@ -1675,11 +1821,15 @@ function renderLayerManagerObject(record, index) {
const removeButton = managedObjectCanRemove ( record )
? ` <button type="button" class="layer-remove-button" aria-label="Rimuovi oggetto dal progetto" title="Rimuovi dal progetto">Rimuovi</button> `
: "" ;
const rowActions = layerManagerRowActions ( renameButton , removeButton ) ;
const moveButton = [ "drawings" , "imported" ] . includes ( record . categoryKey )
? ` <button type="button" class="layer-membership-button" aria-label="Sposta oggetto in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button> `
: "" ;
const rowActions = layerManagerRowActions ( renameButton , moveButton , removeButton ) ;
object . classList . toggle ( "layer-manager-with-actions" , Boolean ( rowActions ) ) ;
const objectName = managedObjectLabel ( record , index ) ;
const locked = Boolean ( record . layer ? . feature ? . properties ? . _lmeLocked ) ;
object . innerHTML = ` <summary><button type="button" class="layer-drag-handle" aria-label="Trascina oggetto">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra oggetto"><span class="layer-manager-title"> ${ escapeHtml ( objectName ) } </span><button type="button" class="layer-inline-lock ${ locked ? "is-locked" : "" } " aria-label=" ${ locked ? "Sblocca" : "Blocca" } oggetto" title=" ${ locked ? "Sblocca" : "Blocca" } "> ${ locked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "oggetto" ) } <span class="layer-manager-count">› </span>< /summary><div class="layer-manager-details"></div> ` ;
object . innerHTML = ` <summary> ${ layerExpandButton ( object . open , objectName ) } <button type="button" class="layer-drag-handle" aria-label="Trascina oggetto">☰</button><input type="checkbox" ${ visible ? "checked" : "" } aria-label="Mostra oggetto"><span class="layer-manager-title"> ${ escapeHtml ( objectName ) } </span><button type="button" class="layer-inline-lock ${ locked ? "is-locked" : "" } " aria-label=" ${ locked ? "Sblocca" : "Blocca" } oggetto" title=" ${ locked ? "Sblocca" : "Blocca" } "> ${ locked ? "🔒" : "🔓" } </button> ${ rowActions } ${ layerManagerMoveButtons ( "oggetto" ) } </summary><div class="layer-manager-details"></div> ` ;
bindLayerManagerToggleButton ( object ) ;
const checkbox = object . querySelector ( "input" ) ;
checkbox . addEventListener ( "click" , event => event . stopPropagation ( ) ) ;
checkbox . addEventListener ( "change" , ( ) => setManagedObjectVisible ( record , checkbox . checked , true ) ) ;
@@ -1693,6 +1843,11 @@ function renderLayerManagerObject(record, index) {
event . stopPropagation ( ) ;
removeManagedRecords ( [ record ] , ` ' ${ objectName } ' ` ) ;
} ) ;
object . querySelector ( ".layer-membership-button" ) ? . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
showMoveManagedRecordsDialog ( [ record ] , ` ' ${ objectName } ' ` ) ;
} ) ;
object . querySelector ( ".layer-inline-lock" ) ? . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
@@ -1701,7 +1856,6 @@ function renderLayerManagerObject(record, index) {
object . querySelector ( ".layer-manager-title" ) . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
object . open = true ;
highlightManagedObject ( record ) ;
} ) ;
const details = object . querySelector ( ".layer-manager-details" ) ;
@@ -1737,7 +1891,7 @@ function renderLayerSelectionToolbar() {
toolbar . innerHTML = ` <label>Cerca oggetti<input type="search" value=" ${ escapeHtml ( lmeState . managerSearchText ) } " placeholder="Nome o gruppo"></label>
<div class="layer-selection-summary"><strong> ${ selectedCount } selezionati</strong><button type="button" data-action="clear">Deseleziona</button></div>
<div class="layer-batch-actions">
<button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="move">Sposta</button>
<button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="move">Sposta geometria </button>
<button type="button" data-action="buffer">Buffer</button><button type="button" data-action="union">Unisci</button>
<button type="button" data-action="split">Dividi</button><button type="button" data-action="delete" class="danger-command">Rimuovi</button>
</div> ` ;
@@ -1826,7 +1980,7 @@ function renderManagedObjectTools(record) {
<label>Note<textarea data-field="notes" rows="2" maxlength="1000" placeholder="Note tecniche"> ${ escapeHtml ( properties . _lmeNotes || "" ) } </textarea></label>
<label class="layer-photo-picker">Foto<input type="file" data-field="photo" accept="image/*"></label>
${ properties . _lmePhoto ? ` <img class="layer-photo-preview" src=" ${ properties . _lmePhoto } " alt="Foto allegata"><button type="button" data-action="remove-photo">Rimuovi foto</button> ` : "" }
<div class="layer-compact-actions"><button type="button" data-action="attributes">Attributi</button><button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="numeric">Coordinate</button><button type="button" data-action="inspect">WGS84 / UTM</button><button type="button" data-action="move">Sposta</button><button type="button" data-action="buffer">Buffer</button></div> ` ;
<div class="layer-compact-actions"><button type="button" data-action="attributes">Attributi</button><button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="numeric">Coordinate</button><button type="button" data-action="inspect">WGS84 / UTM</button><button type="button" data-action="move">Sposta geometria </button><button type="button" data-action="buffer">Buffer</button></div> ` ;
tools . querySelector ( "[data-field='selected']" ) . addEventListener ( "change" , event => {
if ( event . target . checked ) lmeState . managerSelectedObjectIds . add ( record . id ) ;
else lmeState . managerSelectedObjectIds . delete ( record . id ) ;
@@ -1867,25 +2021,35 @@ 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 current = Object . entries ( properties )
. filter ( ( [ key , value ] ) => ! key . startsWith ( "_lme" ) && ! reserved . has ( key ) && [ "string" , "number" , "boolean" ] . includes ( typeof value ) )
. map ( ( [ key , value ] ) => ` ${ key } = ${ value } ` ) . join ( "\n" ) ;
const value = window . prompt ( "A ttributi, uno per riga nel formato chiave=valore\nEsempi: foglio, particella, proprietario, qualita, data_rilievo, stato" , current ) ;
if ( value === null ) return ;
const parsed = { } ;
value . split ( /\r?\n/ ) . forEach ( line => {
const separator = line . indexOf ( "=" ) ;
if ( separator < 1 ) return ;
const key = line . slice ( 0 , separator ) . trim ( ) . replace ( /[^a-zA-Z0-9_ -]/g , "_" ) . slice ( 0 , 60 ) ;
const raw = line . slice ( separator + 1 ) . trim ( ) . slice ( 0 , 500 ) ;
if ( key ) parsed [ key ] = raw ;
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 rows = dialog . querySelector ( ".a ttribute-editor-rows" ) ;
const addRow = ( key = "" , value = "" ) => {
const row = document . createElement ( "div" ) ;
row . className = "attribute-editor-row" ;
row . innerHTML = ` <label>Nome<input type="text" maxlength="60" value=" ${ escapeHtml ( key ) } " placeholder="Es. proprietario"></label><label>Valore<textarea rows="3" maxlength="4000" placeholder="Testo anche su piu righe"> ${ escapeHtml ( String ( value ) ) } </textarea></label><button type="button" title="Rimuovi attributo" aria-label="Rimuovi attributo">× </button> ` ;
row . querySelector ( "button" ) . addEventListener ( "click" , ( ) => row . remove ( ) ) ;
rows . appendChild ( row ) ;
} ;
entries . forEach ( ( [ key , value ] ) => addRow ( key , value ) ) ;
if ( ! entries . length ) addRow ( ) ;
dialog . querySelector ( "[data-add-attribute]" ) . addEventListener ( "click" , ( ) => addRow ( ) ) ;
dialog . querySelector ( "[data-cancel]" ) . addEventListener ( "click" , ( ) => dialog . remove ( ) ) ;
dialog . querySelector ( "[data-save-attributes]" ) . addEventListener ( "click" , ( ) => {
const parsed = { } ;
rows . querySelectorAll ( ".attribute-editor-row" ) . forEach ( row => {
const key = row . querySelector ( "input" ) . value . trim ( ) . replace ( /[^a-zA-Z0-9_ -]/g , "_" ) . slice ( 0 , 60 ) ;
const value = row . querySelector ( "textarea" ) . value . slice ( 0 , 4000 ) ;
if ( key ) parsed [ key ] = value ;
} ) ;
updateManagedProperties ( [ record ] , target => {
Object . keys ( target ) . filter ( key => ! key . startsWith ( "_lme" ) && ! reserved . has ( key ) ) . forEach ( key => delete target [ key ] ) ;
Object . assign ( target , parsed ) ;
} ) ;
setInfo ( ` ${ Object . keys ( parsed ) . length } attributi aggiornati. ` ) ;
dialog . remove ( ) ;
} ) ;
updateManagedProperties ( [ record ] , target => {
Object . keys ( target ) . filter ( key => ! key . startsWith ( "_lme" ) && ! reserved . has ( key ) ) . forEach ( key => delete target [ key ] ) ;
Object . assign ( target , parsed ) ;
} ) ;
finishMapChange ( "Aggiornamento attributi" ) ;
setInfo ( ` ${ Object . keys ( parsed ) . length } attributi aggiornati. ` ) ;
}
function utmFromLatLng ( latitude , longitude ) {
@@ -1949,19 +2113,66 @@ function showAppDialog(title, bodyHtml) {
return overlay ;
}
function showFormDialog ( title , fields , submitLabel = "Salva" ) {
return new Promise ( resolve => {
const fieldHtml = fields . map ( field => {
const common = ` name=" ${ escapeHtml ( field . name ) } " ${ field . required === false ? "" : "required" } ${ field . maxLength ? ` maxlength=" ${ field . maxLength } " ` : "" } ` ;
const control = field . type === "textarea"
? ` <textarea ${ common } rows=" ${ field . rows || 5 } "> ${ escapeHtml ( field . value ? ? "" ) } </textarea> `
: ` <input ${ common } type=" ${ field . type || "text" } " value=" ${ escapeHtml ( field . value ? ? "" ) } " ${ field . step ? ` step=" ${ field . step } " ` : "" } > ` ;
return ` <label> ${ escapeHtml ( field . label ) } ${ control } ${ field . help ? ` <small> ${ escapeHtml ( field . help ) } </small> ` : "" } </label> ` ;
} ) . join ( "" ) ;
const dialog = showAppDialog ( title , ` <form class="lme-edit-form"> ${ fieldHtml } <div class="dialog-command-row"><button type="button" data-cancel>Annulla</button><button type="submit" class="primary-command"> ${ escapeHtml ( submitLabel ) } </button></div></form> ` ) ;
const form = dialog . querySelector ( "form" ) ;
let settled = false ;
const finish = value => { if ( settled ) return ; settled = true ; resolve ( value ) ; dialog . remove ( ) ; } ;
dialog . querySelector ( "header button" ) . addEventListener ( "click" , ( ) => finish ( null ) , { capture : true } ) ;
dialog . querySelector ( "[data-cancel]" ) . addEventListener ( "click" , ( ) => finish ( null ) ) ;
dialog . addEventListener ( "click" , event => { if ( event . target === dialog ) finish ( null ) ; } , { capture : true } ) ;
form . addEventListener ( "submit" , event => {
event . preventDefault ( ) ;
const values = { } ;
fields . forEach ( field => { values [ field . name ] = form . elements [ field . name ] ? . value ? ? "" ; } ) ;
finish ( values ) ;
} ) ;
form . elements [ fields [ 0 ] ? . name ] ? . focus ( ) ;
} ) ;
}
function showObjectTable ( ) {
const records = layerManagerCategories ( ) . flatMap ( category => managedRecordsForCategory ( category ) ) ;
lmeState . managerRecords = new Map ( records . map ( record => [ record . id , record ] ) ) ;
const body = records . map ( ( record , index ) => {
const properties = record . layer ? . feature ? . properties || { } ;
const details = managedObjectDetails ( record . layer ) ;
return ` <tr data-object-id=" ${ escapeHtml ( record . id ) } "><td><button type="button" data-locate=" ${ escapeHtml ( record . id ) } ">⌖</button></td><td> ${ escapeHtml ( managedObjectLabel ( record , index ) ) } </td><td> ${ escapeHtml ( record . categoryKey ) } </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> ` ;
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> ` ;
} ) . 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-wrap"><table><thead><tr><th> </th><th>Nome</th><th>Gruppo</th><th>Foglio</th><th>Particella</th><th>Misure</th></tr></thead><tbody> ${ body || "<tr><td colspan='6 '>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.</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 byId = new Map ( records . map ( record => [ record . id , record ] ) ) ;
dialog . querySelector ( "input" ) . addEventListener ( "input" , event => {
dialog . querySelector ( "input[type='search'] " ) . addEventListener ( "input" , event => {
const query = event . target . value . toLocaleLowerCase ( "it" ) ;
dialog . querySelectorAll ( "tbody tr" ) . forEach ( row => { row . hidden = query && ! row . textContent . toLocaleLowerCase ( "it" ) . includes ( query ) ; } ) ;
} ) ;
dialog . querySelectorAll ( "[data-select]" ) . forEach ( input => input . addEventListener ( "change" , ( ) => {
if ( input . checked ) lmeState . managerSelectedObjectIds . add ( input . dataset . select ) ;
else lmeState . managerSelectedObjectIds . delete ( input . dataset . select ) ;
const record = byId . get ( input . dataset . select ) ;
if ( record && input . checked ) {
lmeState . managerHighlightedObjectId = record . id ;
lmeState . managerHighlightedCategoryKey = record . categoryKey ;
renderManagerHighlight ( [ record ] ) ;
}
refreshLayerManager ( ) ;
setInfo ( ` ${ lmeState . managerSelectedObjectIds . size } oggetti selezionati. ` ) ;
} ) ) ;
dialog . querySelectorAll ( "[data-lock]" ) . forEach ( input => input . addEventListener ( "change" , ( ) => {
const record = byId . get ( input . dataset . lock ) ;
if ( ! record ) return ;
updateManagedProperties ( [ record ] , properties => { properties . _lmeLocked = input . checked ; } ) ;
setInfo ( input . checked ? "Oggetto bloccato." : "Oggetto sbloccato." ) ;
} ) ) ;
dialog . querySelectorAll ( "[data-locate]" ) . forEach ( button => button . addEventListener ( "click" , ( ) => {
const record = byId . get ( button . dataset . locate ) ; if ( ! record ) return ;
dialog . remove ( ) ; highlightManagedObject ( record ) ;
@@ -2088,12 +2299,17 @@ function duplicateManagedRecords(records) {
setInfo ( ` ${ records . length } ${ records . length === 1 ? "oggetto duplicato" : "oggetti duplicati" } . ` ) ;
}
function moveManagedRecords ( records ) {
async function moveManagedRecords ( records ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
if ( records . some ( record => record . layer ? . feature ? . properties ? . _lmeLocked ) ) { setInfo ( "Sblocca gli oggetti da spostare." ) ; return ; }
const north = Number ( window . prompt ( "Spostamento verso nord in metri (negativo verso sud)" , "0" ) ) ;
const result = await showFormDialog ( "Sposta oggetti" , [
{ name : "north" , label : "Metri verso nord (negativo verso sud)" , type : "number" , value : "0" , step : "0.01" } ,
{ name : "east" , label : "Metri verso est (negativo verso ovest)" , type : "number" , value : "0" , step : "0.01" }
] , "Sposta" ) ;
if ( ! result ) return ;
const north = Number ( result . north ) ;
if ( ! Number . isFinite ( north ) ) return ;
const east = Number ( window . prompt ( "Spostamento verso est in metri (negativo verso ovest)" , "0" ) ) ;
const east = Number ( result . east ) ;
if ( ! Number . isFinite ( east ) || ( ! north && ! east ) ) return ;
const distance = Math . hypot ( north , east ) ;
const bearing = Math . atan2 ( east , north ) * 180 / Math . PI ;
@@ -2105,9 +2321,11 @@ function moveManagedRecords(records) {
finishManagedGeometryChange ( ` Spostati ${ records . length } oggetti. ` ) ;
}
function bufferManagedRecords ( records ) {
async function bufferManagedRecords ( records ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
const meters = Number ( window . prompt ( "Distanza buffer in metri" , "5" ) ) ;
const result = await showFormDialog ( "Crea buffer" , [ { name : "meters" , label : "Distanza in metri" , type : "number" , value : "5" , step : "0.01" } ] , "Crea buffer" ) ;
if ( ! result ) return ;
const meters = Number ( result . meters ) ;
if ( ! Number . isFinite ( meters ) || meters <= 0 ) return ;
rememberMapChange ( ) ;
let count = 0 ;
@@ -2158,14 +2376,17 @@ function splitManagedRecords(records) {
}
function selectedManagedRecords ( ) {
const current = lmeState . managerRecords || new Map ( ) ;
const current = new Map ( layerManagerCategories ( )
. flatMap ( category => managedRecordsForCategory ( category ) )
. map ( record => [ record . id , record ] ) ) ;
lmeState . managerRecords = current ;
return [ ... lmeState . managerSelectedObjectIds ] . map ( id => current . get ( id ) ) . filter ( Boolean ) ;
}
function createHoleFromSelected ( ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return false ; }
const polygons = selectedManagedRecords ( ) . filter ( record => record . layer instanceof L . Polygon ) ;
if ( polygons . length !== 2 ) { setInfo ( "Seleziona esattamente due poligoni: prima l'area esterna e poi l'area da escludere." ) ; return false ; }
if ( polygons . length !== 2 ) { showGeometryResult ( "Creazione foro" , "Seleziona esattamente due poligoni: l'area esterna e l'area interna da escludere." , "warning" ) ; return false ; }
try {
const ordered = [ ... polygons ] . sort ( ( left , right ) => turf . area ( right . layer . toGeoJSON ( ) ) - turf . area ( left . layer . toGeoJSON ( ) ) ) ;
const outer = ordered [ 0 ] . layer . toGeoJSON ( ) ;
@@ -2179,14 +2400,15 @@ function createHoleFromSelected() {
ordered . forEach ( removeRecordWithoutPrompt ) ;
lmeState . managerSelectedObjectIds . clear ( ) ;
finishManagedGeometryChange ( "Poligono con foro creato correttamente." ) ;
showGeometryResult ( "Foro creato" , "Il nuovo poligono contiene l'area interna esclusa. I due oggetti originali sono stati sostituiti." , "success" ) ;
return true ;
} catch ( error ) { setInfo ( ` Creazione foro non riuscita: ${ error . message } ` ) ; return false ; }
} catch ( error ) { showGeometryResult ( " Creazione foro non riuscita" , error . message , "error" ) ; return false ; }
}
function validateAndRepairSelected ( ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return "Progetto in sola lettura" ; }
const records = selectedManagedRecords ( ) ;
if ( ! records . length ) { setInfo ( "Seleziona gli oggetti da controllare. " ) ; return "Nessuna selezione" ; }
if ( ! records . length ) { showGeometryResult ( "Controlla e ripara" , " Seleziona uno o più oggetti dalla mappa, dal pannello Layer o dalla tabella oggetti." , "warning ") ; return "Nessuna selezione" ; }
let valid = 0 ; let repaired = 0 ; let rejected = 0 ;
rememberMapChange ( ) ;
records . forEach ( ( record , index ) => {
@@ -2209,14 +2431,16 @@ function validateAndRepairSelected() {
} catch { rejected ++ ; }
} ) ;
lmeState . managerSelectedObjectIds . clear ( ) ;
finishManagedGeometryChange ( ` Controllo geometrie : ${ valid } valide , ${ repaired } riparate , ${ rejected } non riparabili. ` ) ;
const message = ` Controllati ${ records . length } oggetti : ${ valid } validi , ${ repaired } riparati , ${ rejected } non riparabili. ` ;
finishManagedGeometryChange ( message ) ;
showGeometryResult ( "Controllo geometrie completato" , message , rejected ? "warning" : "success" ) ;
return ` ${ valid } valide, ${ repaired } riparate, ${ rejected } non riparabili ` ;
}
function compareSelectedGeometries ( ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return false ; }
const polygons = selectedManagedRecords ( ) . filter ( record => record . layer instanceof L . Polygon ) ;
if ( polygons . length !== 2 ) { setInfo ( "Seleziona esattamente due poligoni da confrontare. " ) ; return false ; }
if ( polygons . length !== 2 ) { showGeometryResult ( "Confronta due confini" , " Seleziona esattamente due poligoni dalla mappa, dal pannello Layer o dalla tabella oggetti." , "warning ") ; return false ; }
try {
const first = polygons [ 0 ] . layer . toGeoJSON ( ) ; const second = polygons [ 1 ] . layer . toGeoJSON ( ) ;
const onlyFirst = turf . difference ( turf . featureCollection ( [ first , second ] ) ) ;
@@ -2231,34 +2455,72 @@ function compareSelectedGeometries() {
addResult ( onlyFirst , ` Solo ${ managedObjectLabel ( polygons [ 0 ] , 0 ) } ` , "#ef4444" ) ;
addResult ( onlySecond , ` Solo ${ managedObjectLabel ( polygons [ 1 ] , 1 ) } ` , "#3b82f6" ) ;
addResult ( overlap , "Sovrapposizione" , "#16a34a" ) ;
const firstArea = onlyFirst ? turf . area ( onlyFirst ) : 0 ; const secondArea = onlySecond ? turf . area ( onlySecond ) : 0 ;
finishManagedGeometryChange ( ` Confronto creato: eccedenza ${ formatArea ( firstArea ) } , mancanza ${ formatArea ( secondArea ) } . ` ) ;
const firstArea = onlyFirst ? turf . area ( onlyFirst ) : 0 ; const secondArea = onlySecond ? turf . area ( onlySecond ) : 0 ; const overlapArea = overlap ? turf . area ( overlap ) : 0 ;
const message = ` Confronto creato. Solo primo confine: ${ formatArea ( firstArea ) } . Solo secondo confine: ${ formatArea ( secondArea ) } . Sovrapposizione: ${ formatArea ( overlapArea ) } . ` ;
finishManagedGeometryChange ( message ) ;
showGeometryResult ( "Confronto completato" , ` ${ message } Sulla mappa: rosso solo primo, blu solo secondo, verde sovrapposizione. ` , "success" ) ;
return true ;
} catch ( error ) { setInfo ( ` Confronto non riuscito: ${ error . message } ` ) ; return false ; }
} catch ( error ) { showGeometryResult ( " Confronto non riuscito" , error . message , "error" ) ; return false ; }
}
function numericEditManagedRecord ( record ) {
function showGeometryResult ( title , message , tone = "success" ) {
setInfo ( ` ${ title } : ${ message } ` ) ;
const icons = { success : "✓" , warning : "!" , error : "× " } ;
showAppDialog ( title , ` <div class="geometry-result geometry-result- ${ tone } "><span aria-hidden="true"> ${ icons [ tone ] || "i" } </span><p> ${ escapeHtml ( message ) } </p></div> ` ) ;
}
function addGeometryToolsControl ( ) {
if ( lmeState . geometryToolsControl || ! lmeState . map ) return ;
const GeometryTools = L . Control . extend ( {
options : { position : "topright" } ,
onAdd ( ) {
const container = L . DomUtil . create ( "div" , "leaflet-bar lme-geometry-toolbar" ) ;
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 }
] ;
actions . forEach ( action => {
const button = L . DomUtil . create ( "a" , "" , container ) ;
button . href = "#" ;
button . title = action . title ;
button . setAttribute ( "role" , "button" ) ;
button . setAttribute ( "aria-label" , action . title ) ;
button . textContent = action . icon ;
L . DomEvent . on ( button , "click" , event => { L . DomEvent . stop ( event ) ; action . run ( ) ; } ) ;
} ) ;
L . DomEvent . disableClickPropagation ( container ) ;
return container ;
}
} ) ;
lmeState . geometryToolsControl = new GeometryTools ( ) . addTo ( lmeState . map ) ;
}
async function numericEditManagedRecord ( record ) {
if ( lmeState . readOnly ) { setInfo ( "Il progetto e' in sola lettura." ) ; return ; }
if ( record . layer ? . feature ? . properties ? . _lmeLocked ) { setInfo ( "Sblocca prima l'oggetto." ) ; return ; }
if ( record . layer instanceof L . Circle ) {
const center = record . layer . getLatLng ( ) ;
const value = window . prompt ( "Latitudine, longitudine, raggio in metri" , ` ${ center . lat . toFixed ( 7 ) } , ${ center . lng . toFixed ( 7 ) } , ${ record . layer . getRadius ( ) . toFixed ( 2 ) } ` ) ;
if ( value === null ) return ;
const result = await showFormDialog ( "Modifica cerchio" , [ { name : "coordinates" , label : " Latitudine, longitudine, raggio in metri", type : "textarea" , rows : 3 , value : ` ${ center . lat . toFixed ( 7 ) } , ${ center . lng . toFixed ( 7 ) } , ${ record . layer . getRadius ( ) . toFixed ( 2 ) } ` } ] , "Applica" ) ;
if ( ! result ) return ;
const value = result . coordinates ;
const parts = value . split ( /[;, ]+/ ) . map ( Number ) . filter ( Number . isFinite ) ;
if ( parts . length < 3 ) { setInfo ( "Inserisci latitudine, longitudine e raggio validi." ) ; return ; }
rememberMapChange ( ) ; record . layer . setLatLng ( [ parts [ 0 ] , parts [ 1 ] ] ) ; record . layer . setRadius ( parts [ 2 ] ) ; annotateDrawingLayer ( record . layer , "circle" ) ;
} else if ( typeof record . layer ? . getLatLng === "function" ) {
const point = record . layer . getLatLng ( ) ;
const value = window . prompt ( "Latitudine, longitudine" , ` ${ point . lat . toFixed ( 7 ) } , ${ point . lng . toFixed ( 7 ) } ` ) ;
if ( value === null ) return ;
const result = await showFormDialog ( "Modifica punto" , [ { name : "coordinates" , label : "Latitudine, longitudine" , type : "textarea" , rows : 3 , value : ` ${ point . lat . toFixed ( 7 ) } , ${ point . lng . toFixed ( 7 ) } ` } ] , "Applica" ) ;
if ( ! result ) return ;
const value = result . coordinates ;
const parts = value . split ( /[;, ]+/ ) . map ( Number ) . filter ( Number . isFinite ) ;
if ( parts . length < 2 ) { setInfo ( "Inserisci coordinate valide." ) ; return ; }
rememberMapChange ( ) ; record . layer . setLatLng ( [ parts [ 0 ] , parts [ 1 ] ] ) ;
} else {
const lines = lineLatLngGroups ( record . layer . getLatLngs ( ) ) ;
const current = lines [ 0 ] ? . map ( point => ` ${ point . lat . toFixed ( 7 ) } , ${ point . lng . toFixed ( 7 ) } ` ) . join ( "; " ) || "" ;
const value = window . prompt ( "Vertici: c oppie latitudine,longitudine separate da punto e virgola" , current ) ;
if ( value === null ) return ;
const result = await showFormDialog ( "Modifica vertici" , [ { name : "coordinates" , label : "C oppie latitudine,longitudine separate da punto e virgola", type : "textarea" , rows : 8 , value : current } ] , "Applica" ) ;
if ( ! result ) return ;
const value = result . coordinates ;
const points = value . split ( ";" ) . map ( pair => pair . split ( "," ) . map ( Number ) ) . filter ( pair => pair . length === 2 && pair . every ( Number . isFinite ) ) ;
if ( points . length < 2 ) { setInfo ( "Servono almeno due vertici validi." ) ; return ; }
rememberMapChange ( ) ; record . layer . setLatLngs ( points . map ( point => L . latLng ( point [ 0 ] , point [ 1 ] ) ) ) ;
@@ -2379,6 +2641,7 @@ function setAdvancedOptions(options) {
lmeState . snapEnabled = value . snapEnabled !== false ;
lmeState . snapDistancePixels = Math . max ( 4 , Math . min ( 40 , Number ( value . snapDistancePixels ) || 14 ) ) ;
lmeState . autoSaveEnabled = value . autoSaveEnabled !== false ;
lmeState . autoFitSelectedObject = Boolean ( value . autoFitSelectedObject ) ;
lmeState . gpsProximityEnabled = value . gpsProximityEnabled !== false ;
lmeState . gpsBoundaryAlertEnabled = Boolean ( value . gpsBoundaryAlertEnabled ) ;
}
@@ -2504,6 +2767,26 @@ function bindLayerManagerExpansion(row, key) {
} else {
lmeState . managerExpandedRows . delete ( key ) ;
}
const button = row . querySelector ( ":scope > summary > .layer-expand-button" ) ;
if ( button ) {
button . textContent = row . open ? "▾" : "▸" ;
button . setAttribute ( "aria-expanded" , String ( row . open ) ) ;
}
} ) ;
}
function layerExpandButton ( expanded , label ) {
return ` <button type="button" class="layer-expand-button" aria-label=" ${ expanded ? "Comprimi" : "Espandi" } ${ escapeHtml ( label ) } " aria-expanded=" ${ expanded } "> ${ expanded ? "▾" : "▸" } </button> ` ;
}
function bindLayerManagerToggleButton ( row ) {
const summary = row . querySelector ( ":scope > summary" ) ;
const button = summary ? . querySelector ( ".layer-expand-button" ) ;
summary ? . addEventListener ( "click" , event => event . preventDefault ( ) ) ;
button ? . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
row . open = ! row . open ;
} ) ;
}
@@ -2603,7 +2886,21 @@ function highlightManagedObject(record) {
} ) ;
setSelectedDrawing ( record . categoryKey === "drawings" ? record . layer : null ) ;
renderManagerHighlight ( [ record ] ) ;
fitLayer ( record . layer ) ;
if ( lmeState . autoFitSelectedObject ) {
fitLayer ( record . layer ) ;
}
revealManagedRecordInLayerManager ( record . id ) ;
}
function revealManagedRecordInLayerManager ( recordId ) {
const row = document . querySelector ( ` .layer-manager-object[data-object-id=" ${ CSS . escape ( recordId ) } "] ` ) ;
if ( ! row ) return ;
const block = row . closest ( ".layer-manager-block" ) ;
const group = row . closest ( ".layer-manager-group" ) ;
if ( group ) group . classList . add ( "layer-manager-selection-path" ) ;
if ( block ) block . classList . add ( "layer-manager-selection-path" ) ;
row . classList . add ( "layer-manager-object-selected" ) ;
row . scrollIntoView ( { block : "nearest" , behavior : "smooth" } ) ;
}
function drawingEditToolbar ( ) {
@@ -2668,7 +2965,6 @@ function bindManagedParentHighlight(row, key, records) {
row . querySelector ( ":scope > summary > .layer-manager-title" ) . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
row . open = true ;
highlightManagedParent ( row , key , records ) ;
} ) ;
summary . addEventListener ( "pointerenter" , ( ) => {
@@ -3404,6 +3700,7 @@ function bindDrawingLayerSelection(layer) {
}
selectDrawingLayer ( layer ) ;
refreshLayerManager ( ) ;
window . requestAnimationFrame ( ( ) => revealManagedRecordInLayerManager ( id ) ) ;
} ) ;
}
@@ -3424,6 +3721,7 @@ function bindManagedMapSelection(layer, categoryKey, parent) {
else lmeState . managerSelectedObjectIds . add ( record . id ) ;
highlightManagedObject ( record ) ;
refreshLayerManager ( ) ;
window . requestAnimationFrame ( ( ) => revealManagedRecordInLayerManager ( record . id ) ) ;
setInfo ( ` Selezionato dalla mappa: ${ managedObjectEditableName ( record ) || "oggetto" } . ` ) ;
} ) ;
}
@@ -4123,23 +4421,6 @@ function emptyFeatureCollection() {
function managedGroupGeoJson ( group , categoryKey ) {
const collection = group ? . toGeoJSON ? . ( ) || emptyFeatureCollection ( ) ;
const features = [ ... ( collection . features || [ ] ) ] ;
const existingIds = new Set ( features . map ( feature => feature ? . properties ? . _lmeManagerId ) . filter ( Boolean ) ) ;
lmeState . managerHiddenObjects . forEach ( record => {
if ( record . categoryKey !== categoryKey ) {
return ;
}
const hidden = record . layer ? . toGeoJSON ? . ( ) ;
const hiddenFeatures = hidden ? . type === "FeatureCollection" ? hidden . features : hidden ? [ hidden ] : [ ] ;
hiddenFeatures . forEach ( feature => {
const id = feature ? . properties ? . _lmeManagerId ;
if ( ! id || ! existingIds . has ( id ) ) {
features . push ( feature ) ;
if ( id ) {
existingIds . add ( id ) ;
}
}
} ) ;
} ) ;
const categoryName = lmeState . managerCategoryNames [ categoryKey ] ;
if ( categoryName ) {
features . forEach ( feature => {