869 lines
28 KiB
JavaScript
869 lines
28 KiB
JavaScript
const lmeState = {
|
|
map: null,
|
|
baseLayers: {},
|
|
currentBase: null,
|
|
currentBaseName: "satellite",
|
|
cadastre: null,
|
|
cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"],
|
|
cadastreOpacity: 0.75,
|
|
imported: null,
|
|
drawings: null,
|
|
drawControl: null,
|
|
drawEventsBound: false,
|
|
gpsMarker: null,
|
|
gpsAccuracy: null,
|
|
gpsTrail: null,
|
|
currentParcelLayer: null,
|
|
gpsTrailEnabled: false,
|
|
gpsTrailPoints: [],
|
|
gpsWatchId: null,
|
|
lastPosition: null,
|
|
fileImportBound: false,
|
|
resizeBound: false,
|
|
googleMapsLoading: null,
|
|
azureMapsKey: null,
|
|
azureAttribution: null
|
|
};
|
|
|
|
window.lmeMap = {
|
|
init,
|
|
showSearchResult,
|
|
displayOfficialParcel,
|
|
exportCadastreView,
|
|
fitImported,
|
|
clearImported,
|
|
addProjectMarker,
|
|
setDrawToolsVisible,
|
|
clearDrawings,
|
|
saveWorkspace,
|
|
loadWorkspace,
|
|
setBaseLayer,
|
|
configureGoogleMaps,
|
|
configureAzureMaps,
|
|
clearAzureMaps,
|
|
setCadastreOpacity,
|
|
toggleCadastreLayer,
|
|
startGps,
|
|
stopGps,
|
|
centerOnGps,
|
|
toggleTrail,
|
|
setNativeGpsPosition,
|
|
setGpsError,
|
|
resize,
|
|
showInfo: setInfo
|
|
};
|
|
|
|
function init() {
|
|
if (lmeState.map) {
|
|
return;
|
|
}
|
|
|
|
lmeState.map = L.map("map", {
|
|
zoomControl: false
|
|
}).setView([41.9028, 12.4964], 17);
|
|
|
|
L.control.zoom({ position: "bottomright" }).addTo(lmeState.map);
|
|
|
|
lmeState.baseLayers.osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
maxNativeZoom: 19,
|
|
maxZoom: 22,
|
|
attribution: "© OpenStreetMap contributors"
|
|
});
|
|
|
|
lmeState.baseLayers.satellite = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", {
|
|
maxNativeZoom: 19,
|
|
maxZoom: 22,
|
|
attribution: "Tiles © Esri"
|
|
});
|
|
|
|
lmeState.baseLayers.topographic = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", {
|
|
maxNativeZoom: 19,
|
|
maxZoom: 22,
|
|
attribution: "Tiles © Esri"
|
|
});
|
|
|
|
lmeState.baseLayers["streets-esri"] = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}", {
|
|
maxNativeZoom: 19,
|
|
maxZoom: 22,
|
|
attribution: "Tiles © Esri"
|
|
});
|
|
|
|
lmeState.baseLayers.terrain = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
|
|
maxNativeZoom: 17,
|
|
maxZoom: 22,
|
|
attribution: "Map data © OpenStreetMap contributors, SRTM | Map style © OpenTopoMap"
|
|
});
|
|
|
|
lmeState.baseLayers.carto = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", {
|
|
subdomains: "abcd",
|
|
maxNativeZoom: 20,
|
|
maxZoom: 22,
|
|
attribution: "© OpenStreetMap contributors © CARTO"
|
|
});
|
|
|
|
lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map);
|
|
lmeState.imported = L.featureGroup().addTo(lmeState.map);
|
|
lmeState.drawings = L.featureGroup().addTo(lmeState.map);
|
|
lmeState.currentParcelLayer = L.featureGroup().addTo(lmeState.map);
|
|
lmeState.gpsTrail = L.polyline([], { color: "#f97316", weight: 4, opacity: 0.85 }).addTo(lmeState.map);
|
|
|
|
refreshCadastreLayer();
|
|
bindFileImport();
|
|
|
|
if (!lmeState.resizeBound) {
|
|
window.addEventListener("resize", resize);
|
|
window.addEventListener("orientationchange", resize);
|
|
lmeState.resizeBound = true;
|
|
}
|
|
|
|
lmeState.map.on("click", async event => {
|
|
const info = await queryFeatureInfo(event.latlng);
|
|
if (info) {
|
|
L.popup().setLatLng(event.latlng).setContent(info).openOn(lmeState.map);
|
|
}
|
|
});
|
|
lmeState.map.on("moveend", () => void updateAzureAttribution());
|
|
|
|
setInfo("Mappa pronta: satellite + catasto AdE");
|
|
}
|
|
|
|
function showSearchResult(latitude, longitude, label) {
|
|
const lat = Number(latitude);
|
|
const lon = Number(longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
|
setInfo("La ricerca ha restituito coordinate non valide.");
|
|
return;
|
|
}
|
|
|
|
lmeState.map.setView([lat, lon], 18);
|
|
setInfo(label || `${lat.toFixed(6)}, ${lon.toFixed(6)}`);
|
|
}
|
|
|
|
function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shouldFit) {
|
|
const lat = Number(latitude);
|
|
const lon = Number(longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
|
setInfo("Coordinate della particella non valide.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const geojson = gmlParcelsToGeoJson(xmlText);
|
|
const normalizedParcel = normalizeParcel(parcel);
|
|
const featureByNumber = normalizedParcel
|
|
? (geojson.features || []).find(item => {
|
|
const props = item.properties || {};
|
|
return String(props.LABEL || "").trim().toUpperCase() === normalizedParcel
|
|
|| String(props.NATIONALCADASTRALREFERENCE || "").toUpperCase().endsWith(`.${normalizedParcel}`);
|
|
})
|
|
: null;
|
|
const feature = featureByNumber
|
|
|| (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [lon, lat]))
|
|
|| geojson.features?.[0];
|
|
|
|
if (!feature) {
|
|
if (shouldFit) {
|
|
lmeState.map.setView([lat, lon], 20);
|
|
}
|
|
setInfo("Posizione trovata, ma il servizio WFS dell'Agenzia non ha restituito il confine. Riprova tra poco.");
|
|
return;
|
|
}
|
|
|
|
lmeState.currentParcelLayer.clearLayers();
|
|
const layer = L.geoJSON(feature, {
|
|
style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 }
|
|
}).addTo(lmeState.currentParcelLayer);
|
|
if (shouldFit) {
|
|
fitLayer(layer);
|
|
}
|
|
|
|
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || label;
|
|
if (normalizedParcel) {
|
|
setText("parcelState", `Particella selezionata: ${ref}`);
|
|
setInfo(`Trovata ${ref}. Ora puoi attivare il GPS e camminare dentro il confine evidenziato.`);
|
|
} else {
|
|
setText("parcelState", `Dentro particella AdE: ${ref}`);
|
|
}
|
|
} catch (error) {
|
|
setInfo(`Geometria catastale non leggibile: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function normalizeParcel(parcel) {
|
|
return String(parcel || "").trim().toUpperCase();
|
|
}
|
|
|
|
function resize() {
|
|
if (!lmeState.map) {
|
|
return;
|
|
}
|
|
window.setTimeout(() => lmeState.map.invalidateSize({ pan: false }), 50);
|
|
}
|
|
|
|
function exportCadastreView() {
|
|
const center = lmeState.map.getCenter();
|
|
const zoom = lmeState.map.getZoom();
|
|
const layers = lmeState.cadastreLayers.join(",");
|
|
const url = `${cadastreUrl()}service=WMS&version=1.3.0&request=GetMap&layers=${encodeURIComponent(layers)}&styles=&format=image/png&transparent=true&width=1200&height=1200&crs=EPSG:4258&bbox=${center.lat - 0.01},${center.lng - 0.01},${center.lat + 0.01},${center.lng + 0.01}`;
|
|
window.open(url, "_blank");
|
|
setInfo(`Vista catastale aperta per download. Zoom corrente: ${zoom}.`);
|
|
}
|
|
|
|
function bindFileImport() {
|
|
if (lmeState.fileImportBound) {
|
|
return;
|
|
}
|
|
|
|
document.addEventListener("change", async event => {
|
|
if (event.target?.id !== "gisFileInput") {
|
|
return;
|
|
}
|
|
|
|
const file = event.target.files?.[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const geojson = await readGisFile(file);
|
|
const layer = L.geoJSON(geojson, {
|
|
style: { color: "#22c55e", weight: 3, opacity: 0.95, fillOpacity: 0.18 },
|
|
pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 7, color: "#22c55e", fillColor: "#ffffff", fillOpacity: 0.9 }),
|
|
onEachFeature: (feature, layer) => {
|
|
const name = feature?.properties?.name || file.name;
|
|
layer.bindPopup(`<strong>${escapeHtml(name)}</strong>`);
|
|
}
|
|
}).addTo(lmeState.imported);
|
|
fitLayer(layer);
|
|
setInfo(`Caricato ${file.name}`);
|
|
updateGpsInsideState();
|
|
} catch (error) {
|
|
setInfo(`File non leggibile: ${error.message}`);
|
|
}
|
|
});
|
|
lmeState.fileImportBound = true;
|
|
}
|
|
|
|
async function readGisFile(file) {
|
|
const lowerName = file.name.toLowerCase();
|
|
|
|
if (lowerName.endsWith(".geojson") || lowerName.endsWith(".json")) {
|
|
return JSON.parse(await file.text());
|
|
}
|
|
|
|
if (lowerName.endsWith(".kml")) {
|
|
return kmlTextToGeoJson(await file.text());
|
|
}
|
|
|
|
if (lowerName.endsWith(".kmz")) {
|
|
const zip = await JSZip.loadAsync(await file.arrayBuffer());
|
|
const kmlName = Object.keys(zip.files).find(name => name.toLowerCase().endsWith(".kml"));
|
|
if (!kmlName) {
|
|
throw new Error("KMZ senza file KML interno");
|
|
}
|
|
return kmlTextToGeoJson(await zip.files[kmlName].async("text"));
|
|
}
|
|
|
|
throw new Error("Formato supportato: KML, KMZ, GeoJSON");
|
|
}
|
|
|
|
function kmlTextToGeoJson(text) {
|
|
const xml = new DOMParser().parseFromString(text, "text/xml");
|
|
return toGeoJSON.kml(xml);
|
|
}
|
|
|
|
function fitImported() {
|
|
fitLayer(lmeState.imported);
|
|
}
|
|
|
|
function clearImported() {
|
|
lmeState.imported.clearLayers();
|
|
setInfo("File GIS rimossi.");
|
|
updateGpsInsideState();
|
|
}
|
|
|
|
function addProjectMarker(name) {
|
|
const center = lmeState.map.getCenter();
|
|
const label = (name || "Nuovo POI").trim();
|
|
const marker = L.marker(center, { draggable: true }).addTo(lmeState.drawings);
|
|
marker.bindPopup(`<strong>${escapeHtml(label)}</strong><br>${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}`).openPopup();
|
|
setInfo(`POI aggiunto: ${label}`);
|
|
}
|
|
|
|
function enableDrawTools() {
|
|
if (lmeState.drawControl) {
|
|
setInfo("Strumenti di disegno gia attivi.");
|
|
return;
|
|
}
|
|
|
|
lmeState.drawControl = new L.Control.Draw({
|
|
position: "topright",
|
|
edit: { featureGroup: lmeState.drawings },
|
|
draw: {
|
|
circle: false,
|
|
circlemarker: false,
|
|
marker: true,
|
|
polygon: true,
|
|
polyline: true,
|
|
rectangle: true
|
|
}
|
|
});
|
|
lmeState.map.addControl(lmeState.drawControl);
|
|
if (!lmeState.drawEventsBound) {
|
|
lmeState.map.on(L.Draw.Event.CREATED, event => {
|
|
lmeState.drawings.addLayer(event.layer);
|
|
updateGpsInsideState();
|
|
});
|
|
lmeState.drawEventsBound = true;
|
|
}
|
|
setInfo("Disegno attivo: crea POI, linee e aree sulla mappa.");
|
|
}
|
|
|
|
function setDrawToolsVisible(visible) {
|
|
if (visible) {
|
|
enableDrawTools();
|
|
return;
|
|
}
|
|
|
|
if (!lmeState.drawControl) {
|
|
return;
|
|
}
|
|
|
|
Object.values(lmeState.drawControl._toolbars || {}).forEach(toolbar => {
|
|
if (typeof toolbar.disable === "function") {
|
|
toolbar.disable();
|
|
}
|
|
});
|
|
lmeState.map.removeControl(lmeState.drawControl);
|
|
lmeState.drawControl = null;
|
|
setInfo("Strumenti di disegno nascosti.");
|
|
}
|
|
|
|
function clearDrawings() {
|
|
const count = lmeState.drawings.getLayers().length;
|
|
if (!count) {
|
|
setInfo("Non ci sono disegni o POI da eliminare.");
|
|
return;
|
|
}
|
|
|
|
if (!window.confirm(`Eliminare ${count} disegni e POI dalla mappa?`)) {
|
|
return;
|
|
}
|
|
|
|
lmeState.drawings.clearLayers();
|
|
updateGpsInsideState();
|
|
setInfo("Disegni e POI eliminati dalla mappa.");
|
|
}
|
|
|
|
function saveWorkspace() {
|
|
const data = {
|
|
drawings: lmeState.drawings.toGeoJSON(),
|
|
imported: lmeState.imported.toGeoJSON(),
|
|
trail: lmeState.gpsTrailPoints
|
|
};
|
|
localStorage.setItem("lmeMapWorkspace", JSON.stringify(data));
|
|
setInfo("Progetti, POI, file e percorso salvati sul dispositivo.");
|
|
}
|
|
|
|
function loadWorkspace() {
|
|
const raw = localStorage.getItem("lmeMapWorkspace");
|
|
if (!raw) {
|
|
setInfo("Nessun progetto salvato trovato.");
|
|
return;
|
|
}
|
|
|
|
const data = JSON.parse(raw);
|
|
lmeState.drawings.clearLayers();
|
|
lmeState.imported.clearLayers();
|
|
L.geoJSON(data.drawings, { style: { color: "#f97316", weight: 3, fillOpacity: 0.16 } }).addTo(lmeState.drawings);
|
|
L.geoJSON(data.imported, { style: { color: "#22c55e", weight: 3, fillOpacity: 0.18 } }).addTo(lmeState.imported);
|
|
lmeState.gpsTrailPoints = data.trail || [];
|
|
lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
|
|
fitLayer(L.featureGroup([lmeState.drawings, lmeState.imported]));
|
|
updateGpsInsideState();
|
|
setInfo("Progetto caricato.");
|
|
}
|
|
|
|
function setBaseLayer(name) {
|
|
const selected = lmeState.baseLayers[name] || lmeState.baseLayers.satellite;
|
|
if (lmeState.currentBase) {
|
|
lmeState.map.removeLayer(lmeState.currentBase);
|
|
}
|
|
lmeState.currentBase = selected.addTo(lmeState.map);
|
|
lmeState.currentBaseName = selected === lmeState.baseLayers[name] ? name : "satellite";
|
|
lmeState.currentBase.bringToBack();
|
|
const labels = {
|
|
satellite: "Satellite Esri",
|
|
osm: "OpenStreetMap",
|
|
"streets-esri": "Strade dettagliate Esri",
|
|
topographic: "Topografica Esri",
|
|
terrain: "OpenTopoMap",
|
|
carto: "CARTO Voyager",
|
|
"azure-road": "Strade Azure Maps",
|
|
"azure-satellite": "Satellite Azure Maps",
|
|
"google-satellite": "Satellite Google Maps",
|
|
"google-roadmap": "Strade Google Maps"
|
|
};
|
|
setInfo(`Sfondo attivo: ${labels[name] || labels.satellite}. Il catasto AdE resta sovrapposto.`);
|
|
if (lmeState.currentBaseName.startsWith("azure-")) {
|
|
void updateAzureAttribution();
|
|
} else {
|
|
clearAzureAttribution();
|
|
}
|
|
}
|
|
|
|
function configureGoogleMaps(apiKey) {
|
|
const key = (apiKey || "").trim();
|
|
if (!key) {
|
|
return Promise.reject(new Error("Chiave Google Maps mancante."));
|
|
}
|
|
|
|
if (window.google?.maps && L.gridLayer.googleMutant) {
|
|
createGoogleLayers();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (lmeState.googleMapsLoading) {
|
|
return lmeState.googleMapsLoading;
|
|
}
|
|
|
|
lmeState.googleMapsLoading = new Promise((resolve, reject) => {
|
|
const callbackName = `lmeGoogleMapsReady${Date.now()}`;
|
|
const script = document.createElement("script");
|
|
window[callbackName] = () => {
|
|
delete window[callbackName];
|
|
createGoogleLayers();
|
|
resolve();
|
|
};
|
|
script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(key)}&loading=async&callback=${callbackName}`;
|
|
script.async = true;
|
|
script.onerror = () => {
|
|
delete window[callbackName];
|
|
lmeState.googleMapsLoading = null;
|
|
reject(new Error("Impossibile caricare Google Maps. Controlla chiave, API e connessione."));
|
|
};
|
|
document.head.appendChild(script);
|
|
});
|
|
|
|
return lmeState.googleMapsLoading;
|
|
}
|
|
|
|
function createGoogleLayers() {
|
|
if (!L.gridLayer.googleMutant) {
|
|
throw new Error("Modulo Google Maps non disponibile.");
|
|
}
|
|
|
|
lmeState.baseLayers["google-satellite"] ||= L.gridLayer.googleMutant({ type: "hybrid", maxZoom: 22 });
|
|
lmeState.baseLayers["google-roadmap"] ||= L.gridLayer.googleMutant({ type: "roadmap", maxZoom: 22 });
|
|
}
|
|
|
|
function configureAzureMaps(subscriptionKey) {
|
|
const key = (subscriptionKey || "").trim();
|
|
if (!key) {
|
|
throw new Error("Chiave Azure Maps mancante.");
|
|
}
|
|
if (lmeState.azureMapsKey === key
|
|
&& lmeState.baseLayers["azure-road"]
|
|
&& lmeState.baseLayers["azure-satellite"]) {
|
|
return;
|
|
}
|
|
|
|
const activeAzureLayer = lmeState.currentBaseName.startsWith("azure-")
|
|
? lmeState.currentBaseName
|
|
: null;
|
|
if (activeAzureLayer && lmeState.currentBase) {
|
|
lmeState.map.removeLayer(lmeState.currentBase);
|
|
}
|
|
|
|
lmeState.azureMapsKey = key;
|
|
lmeState.baseLayers["azure-road"] = createAzureLayer(key, "microsoft.base.road", 22);
|
|
lmeState.baseLayers["azure-satellite"] = createAzureLayer(key, "microsoft.imagery", 19);
|
|
|
|
if (activeAzureLayer) {
|
|
lmeState.currentBase = lmeState.baseLayers[activeAzureLayer].addTo(lmeState.map);
|
|
lmeState.currentBase.bringToBack();
|
|
void updateAzureAttribution();
|
|
}
|
|
}
|
|
|
|
function clearAzureMaps() {
|
|
if (lmeState.currentBaseName.startsWith("azure-") && lmeState.currentBase) {
|
|
lmeState.map.removeLayer(lmeState.currentBase);
|
|
lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map);
|
|
lmeState.currentBase.bringToBack();
|
|
lmeState.currentBaseName = "satellite";
|
|
}
|
|
|
|
delete lmeState.baseLayers["azure-road"];
|
|
delete lmeState.baseLayers["azure-satellite"];
|
|
lmeState.azureMapsKey = null;
|
|
clearAzureAttribution();
|
|
}
|
|
|
|
function createAzureLayer(key, tilesetId, maxNativeZoom) {
|
|
const url = `https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=${tilesetId}&zoom={z}&x={x}&y={y}&tileSize=256&language=it-IT&view=Auto&subscription-key=${encodeURIComponent(key)}`;
|
|
const layer = L.tileLayer(url, {
|
|
minZoom: tilesetId === "microsoft.imagery" ? 1 : 0,
|
|
maxNativeZoom,
|
|
maxZoom: 22,
|
|
attribution: "Microsoft Azure Maps"
|
|
});
|
|
let errorShown = false;
|
|
layer.on("tileerror", () => {
|
|
if (!errorShown) {
|
|
errorShown = true;
|
|
setInfo("Azure Maps non ha restituito le tessere. Controlla quota e permessi Render della chiave.");
|
|
}
|
|
});
|
|
return layer;
|
|
}
|
|
|
|
async function updateAzureAttribution() {
|
|
if (!lmeState.azureMapsKey || !lmeState.currentBaseName.startsWith("azure-")) {
|
|
return;
|
|
}
|
|
|
|
const requestedLayer = lmeState.currentBaseName;
|
|
const tilesetId = requestedLayer === "azure-satellite" ? "microsoft.imagery" : "microsoft.base.road";
|
|
const bounds = lmeState.map.getBounds();
|
|
const zoom = Math.min(lmeState.map.getZoom(), tilesetId === "microsoft.imagery" ? 19 : 22);
|
|
const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()].join(",");
|
|
const url = `https://atlas.microsoft.com/map/attribution?api-version=2024-04-01&tilesetId=${tilesetId}&zoom=${zoom}&bounds=${bbox}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
"subscription-key": lmeState.azureMapsKey
|
|
}
|
|
});
|
|
if (!response.ok) {
|
|
return;
|
|
}
|
|
const payload = await response.json();
|
|
if (lmeState.currentBaseName !== requestedLayer) {
|
|
return;
|
|
}
|
|
|
|
const attribution = [...new Set((payload.copyrights || []).map(htmlToText).filter(Boolean))].join(" | ");
|
|
setAzureAttribution(attribution);
|
|
} catch {
|
|
clearAzureAttribution();
|
|
}
|
|
}
|
|
|
|
function setAzureAttribution(text) {
|
|
clearAzureAttribution();
|
|
if (!text) {
|
|
return;
|
|
}
|
|
lmeState.azureAttribution = escapeHtml(text);
|
|
lmeState.map.attributionControl.addAttribution(lmeState.azureAttribution);
|
|
}
|
|
|
|
function clearAzureAttribution() {
|
|
if (lmeState.azureAttribution) {
|
|
lmeState.map.attributionControl.removeAttribution(lmeState.azureAttribution);
|
|
lmeState.azureAttribution = null;
|
|
}
|
|
}
|
|
|
|
function htmlToText(value) {
|
|
const element = document.createElement("div");
|
|
element.innerHTML = String(value || "");
|
|
return element.textContent?.trim() || "";
|
|
}
|
|
|
|
function setCadastreOpacity(value) {
|
|
lmeState.cadastreOpacity = Number(value || 0.75);
|
|
if (lmeState.cadastre) {
|
|
lmeState.cadastre.setOpacity(lmeState.cadastreOpacity);
|
|
}
|
|
}
|
|
|
|
function toggleCadastreLayer(layer, enabled) {
|
|
const exists = lmeState.cadastreLayers.includes(layer);
|
|
if (enabled && !exists) {
|
|
lmeState.cadastreLayers.push(layer);
|
|
}
|
|
if (!enabled && exists) {
|
|
lmeState.cadastreLayers = lmeState.cadastreLayers.filter(item => item !== layer);
|
|
}
|
|
refreshCadastreLayer();
|
|
}
|
|
|
|
function refreshCadastreLayer() {
|
|
if (lmeState.cadastre) {
|
|
lmeState.map.removeLayer(lmeState.cadastre);
|
|
}
|
|
|
|
if (!lmeState.cadastreLayers.length) {
|
|
return;
|
|
}
|
|
|
|
lmeState.cadastre = L.tileLayer.wms(cadastreUrl(), {
|
|
layers: lmeState.cadastreLayers.join(","),
|
|
format: "image/png",
|
|
transparent: true,
|
|
version: "1.3.0",
|
|
crs: L.CRS.EPSG4326,
|
|
maxZoom: 22,
|
|
opacity: lmeState.cadastreOpacity,
|
|
attribution: "Agenzia delle Entrate - CC BY 4.0"
|
|
}).addTo(lmeState.map);
|
|
}
|
|
|
|
function startGps() {
|
|
if (!navigator.geolocation) {
|
|
setInfo("Geolocalizzazione non disponibile sul dispositivo.");
|
|
setText("gpsState", "GPS non disponibile");
|
|
return;
|
|
}
|
|
|
|
if (lmeState.gpsWatchId !== null) {
|
|
setInfo("Tracking GPS gia attivo.");
|
|
return;
|
|
}
|
|
|
|
lmeState.gpsWatchId = navigator.geolocation.watchPosition(onGpsPosition, onGpsError, {
|
|
enableHighAccuracy: true,
|
|
maximumAge: 1000,
|
|
timeout: 15000
|
|
});
|
|
setText("gpsState", "Aggancio GPS in corso...");
|
|
setInfo("Tracking GPS avviato.");
|
|
}
|
|
|
|
function stopGps() {
|
|
if (lmeState.gpsWatchId !== null) {
|
|
navigator.geolocation.clearWatch(lmeState.gpsWatchId);
|
|
lmeState.gpsWatchId = null;
|
|
}
|
|
setText("gpsState", "GPS fermo");
|
|
setInfo("Tracking GPS fermato.");
|
|
}
|
|
|
|
function setNativeGpsPosition(location) {
|
|
onGpsPosition({
|
|
coords: {
|
|
latitude: location.latitude,
|
|
longitude: location.longitude,
|
|
accuracy: location.accuracy || 0,
|
|
altitude: location.altitude,
|
|
heading: location.course,
|
|
speed: location.speed
|
|
},
|
|
timestamp: location.timestamp
|
|
});
|
|
}
|
|
|
|
function setGpsError(error) {
|
|
setText("gpsState", "GPS non disponibile");
|
|
setInfo(error || "Errore GPS.");
|
|
}
|
|
|
|
function centerOnGps() {
|
|
if (!lmeState.lastPosition) {
|
|
setInfo("Nessuna posizione GPS ancora disponibile.");
|
|
return;
|
|
}
|
|
lmeState.map.setView(lmeState.lastPosition, Math.max(lmeState.map.getZoom(), 19));
|
|
}
|
|
|
|
function toggleTrail(enabled) {
|
|
lmeState.gpsTrailEnabled = enabled;
|
|
if (!enabled) {
|
|
setInfo("Registrazione percorso disattivata.");
|
|
}
|
|
}
|
|
|
|
function onGpsPosition(position) {
|
|
const { latitude, longitude, accuracy } = position.coords;
|
|
const latlng = [latitude, longitude];
|
|
lmeState.lastPosition = latlng;
|
|
|
|
if (!lmeState.gpsMarker) {
|
|
lmeState.gpsMarker = L.marker(latlng, { title: "La tua posizione" }).addTo(lmeState.map);
|
|
lmeState.gpsAccuracy = L.circle(latlng, { radius: accuracy, color: "#2563eb", weight: 2, fillColor: "#60a5fa", fillOpacity: 0.16 }).addTo(lmeState.map);
|
|
lmeState.map.setView(latlng, 19);
|
|
} else {
|
|
lmeState.gpsMarker.setLatLng(latlng);
|
|
lmeState.gpsAccuracy.setLatLng(latlng).setRadius(accuracy);
|
|
}
|
|
|
|
if (lmeState.gpsTrailEnabled) {
|
|
lmeState.gpsTrailPoints.push(latlng);
|
|
lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
|
|
}
|
|
|
|
setText("gpsState", `${latitude.toFixed(6)}, ${longitude.toFixed(6)} - precisione ${Math.round(accuracy)} m`);
|
|
updateGpsInsideState();
|
|
}
|
|
|
|
function onGpsError(error) {
|
|
setText("gpsState", "GPS non autorizzato o non disponibile");
|
|
setInfo(error.message || "Errore GPS.");
|
|
}
|
|
|
|
function updateGpsInsideState() {
|
|
if (!lmeState.lastPosition) {
|
|
return;
|
|
}
|
|
|
|
const point = [lmeState.lastPosition[1], lmeState.lastPosition[0]];
|
|
const officialMatch = findContainingPolygon(point, lmeState.currentParcelLayer.toGeoJSON());
|
|
if (officialMatch) {
|
|
const reference = officialMatch.properties?.NATIONALCADASTRALREFERENCE
|
|
|| officialMatch.properties?.INSPIREID_LOCALID
|
|
|| officialMatch.properties?.LABEL
|
|
|| "particella AdE";
|
|
setText("parcelState", `Dentro particella AdE: ${reference}`);
|
|
return;
|
|
}
|
|
|
|
const match = findContainingPolygon(point, lmeState.imported.toGeoJSON()) || findContainingPolygon(point, lmeState.drawings.toGeoJSON());
|
|
|
|
if (match) {
|
|
const name = match.properties?.name || match.properties?.Name || "area caricata/disegnata";
|
|
setText("parcelState", `Dentro: ${name}`);
|
|
return;
|
|
}
|
|
|
|
setText(
|
|
"parcelState",
|
|
lmeState.currentParcelLayer.getLayers().length
|
|
? "Fuori dalla particella catastale evidenziata"
|
|
: "Fuori dai poligoni caricati");
|
|
}
|
|
|
|
function gmlParcelsToGeoJson(xmlText) {
|
|
const xml = new DOMParser().parseFromString(xmlText, "text/xml");
|
|
const members = Array.from(xml.getElementsByTagNameNS("*", "member"));
|
|
const features = members.map(member => parcelMemberToFeature(member)).filter(Boolean);
|
|
return { type: "FeatureCollection", features };
|
|
}
|
|
|
|
function parcelMemberToFeature(member) {
|
|
const posList = member.getElementsByTagNameNS("*", "posList")[0]?.textContent?.trim();
|
|
if (!posList) {
|
|
return null;
|
|
}
|
|
|
|
const values = posList.split(/\s+/).map(Number).filter(value => !Number.isNaN(value));
|
|
const ring = [];
|
|
for (let index = 0; index < values.length; index += 2) {
|
|
ring.push([values[index + 1], values[index]]);
|
|
}
|
|
|
|
const properties = {};
|
|
["INSPIREID_LOCALID", "INSPIREID_NAMESPACE", "LABEL", "NATIONALCADASTRALREFERENCE", "ADMINISTRATIVEUNIT"].forEach(name => {
|
|
const value = member.getElementsByTagNameNS("*", name)[0]?.textContent;
|
|
if (value) {
|
|
properties[name] = value;
|
|
}
|
|
});
|
|
|
|
return {
|
|
type: "Feature",
|
|
properties,
|
|
geometry: {
|
|
type: "Polygon",
|
|
coordinates: [ring]
|
|
}
|
|
};
|
|
}
|
|
|
|
function findContainingPolygon(point, geojson) {
|
|
const features = geojson?.features || [];
|
|
return features.find(feature => feature.geometry && geometryContainsPoint(feature.geometry, point));
|
|
}
|
|
|
|
function geometryContainsPoint(geometry, point) {
|
|
if (geometry.type === "Polygon") {
|
|
return polygonContainsPoint(geometry.coordinates, point);
|
|
}
|
|
if (geometry.type === "MultiPolygon") {
|
|
return geometry.coordinates.some(polygon => polygonContainsPoint(polygon, point));
|
|
}
|
|
if (geometry.type === "GeometryCollection") {
|
|
return geometry.geometries.some(item => geometryContainsPoint(item, point));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function polygonContainsPoint(rings, point) {
|
|
if (!rings.length || !ringContainsPoint(rings[0], point)) {
|
|
return false;
|
|
}
|
|
return !rings.slice(1).some(ring => ringContainsPoint(ring, point));
|
|
}
|
|
|
|
function ringContainsPoint(ring, point) {
|
|
const [x, y] = point;
|
|
let inside = false;
|
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
const xi = ring[i][0], yi = ring[i][1];
|
|
const xj = ring[j][0], yj = ring[j][1];
|
|
const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
|
if (intersect) {
|
|
inside = !inside;
|
|
}
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
async function queryFeatureInfo(latlng) {
|
|
if (!lmeState.cadastreLayers.includes("CP.CadastralParcel")) {
|
|
return "";
|
|
}
|
|
|
|
const point = lmeState.map.latLngToContainerPoint(latlng, lmeState.map.getZoom());
|
|
const size = lmeState.map.getSize();
|
|
const bbox = lmeState.map.getBounds();
|
|
const wmsBbox = `${bbox.getSouth()},${bbox.getWest()},${bbox.getNorth()},${bbox.getEast()}`;
|
|
const url = `${cadastreUrl()}service=WMS&version=1.3.0&request=GetFeatureInfo&layers=CP.CadastralParcel&query_layers=CP.CadastralParcel&styles=&crs=EPSG:4258&bbox=${wmsBbox}&width=${size.x}&height=${size.y}&i=${Math.round(point.x)}&j=${Math.round(point.y)}&info_format=text/html`;
|
|
|
|
try {
|
|
const response = await fetch(url);
|
|
const text = await response.text();
|
|
return text.includes("<body") ? text : escapeHtml(text);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function fitLayer(layer) {
|
|
try {
|
|
const bounds = layer.getBounds();
|
|
if (bounds.isValid()) {
|
|
lmeState.map.fitBounds(bounds.pad(0.15));
|
|
}
|
|
} catch {
|
|
setInfo("Nessuna geometria da inquadrare.");
|
|
}
|
|
}
|
|
|
|
function cadastreUrl() {
|
|
return "https://wms.cartografia.agenziaentrate.gov.it/inspire/wms/ows01.php?language=ita&";
|
|
}
|
|
|
|
function setInfo(text) {
|
|
setText("mapInfo", text);
|
|
}
|
|
|
|
function setText(id, text) {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.textContent = text;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|