Initial Android cadastral map app
This commit is contained in:
commit
bc4e82b518
43 files changed
+2137
No files matched your search
@@ -0,0 +1,285 @@
|
||||
html,
|
||||
body,
|
||||
#app,
|
||||
.app-shell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
background: #101820;
|
||||
color: #17212b;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 38px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
background: #176b87;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid #c8d3dc;
|
||||
border-radius: 6px;
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
color: #17212b;
|
||||
}
|
||||
|
||||
input[type="range"],
|
||||
input[type="checkbox"] {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.map-app {
|
||||
display: grid;
|
||||
grid-template-columns: 390px minmax(0, 1fr);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
z-index: 800;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
background: #f6f8f9;
|
||||
border-right: 1px solid #d8e0e6;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 3px;
|
||||
color: #667785;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
font-size: 25px;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.status {
|
||||
border-radius: 999px;
|
||||
padding: 5px 9px;
|
||||
background: #dff5e5;
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
padding: 12px;
|
||||
border: 1px solid #dbe4ea;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input-row,
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row input {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-grid.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: #475866;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 10px 0 0;
|
||||
color: #667785;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.file-picker {
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
place-items: center;
|
||||
border: 1px dashed #93a7b5;
|
||||
border-radius: 8px;
|
||||
background: #f2f7fa;
|
||||
color: #176b87;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-picker input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layer-toggles {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.layer-toggles label,
|
||||
.toggle-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.gps-readout {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #eef6ff;
|
||||
color: #21405d;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gps-readout strong {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.map-stage {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #d4dde3;
|
||||
}
|
||||
|
||||
.map-info {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 700;
|
||||
max-width: 760px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(16, 24, 32, 0.86);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #667785;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: #fff3cd;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.status-bar-safe-area {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 850px) {
|
||||
.map-app {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 45% 55%;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #d8e0e6;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.status-bar-safe-area {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: env(safe-area-inset-top);
|
||||
background-color: #f6f8f9;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
@charset "UTF-8";/*!
|
||||
* Bootstrap v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){leLine truncated
|
||||
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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 truncated
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>LME Mappe Catastali</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="LMEMappeCatastaliApp.styles.css" />
|
||||
<link rel="icon" href="data:,">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="status-bar-safe-area"></div>
|
||||
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
Si e' verificato un errore.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
<script src="https://unpkg.com/@tmcw/togeojson@5.8.1/dist/togeojson.umd.js"></script>
|
||||
<script src="js/lme-map.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,759 @@
|
||||
const lmeState = {
|
||||
map: null,
|
||||
baseLayers: {},
|
||||
currentBase: null,
|
||||
cadastre: null,
|
||||
cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"],
|
||||
cadastreOpacity: 0.75,
|
||||
imported: null,
|
||||
drawings: null,
|
||||
drawControl: null,
|
||||
gpsMarker: null,
|
||||
gpsAccuracy: null,
|
||||
gpsTrail: null,
|
||||
currentParcelLayer: null,
|
||||
gpsTrailEnabled: true,
|
||||
gpsTrailPoints: [],
|
||||
gpsWatchId: null,
|
||||
lastPosition: null,
|
||||
lastWfsLookupAt: 0,
|
||||
wfsLookupInFlight: false,
|
||||
duckDb: null,
|
||||
duckDbConnection: null
|
||||
};
|
||||
|
||||
window.lmeMap = {
|
||||
init,
|
||||
search,
|
||||
locateParcelArea,
|
||||
exportCadastreView,
|
||||
fitImported,
|
||||
clearImported,
|
||||
addProjectMarker,
|
||||
enableDrawTools,
|
||||
saveWorkspace,
|
||||
loadWorkspace,
|
||||
setBaseLayer,
|
||||
setCadastreOpacity,
|
||||
toggleCadastreLayer,
|
||||
startGps,
|
||||
stopGps,
|
||||
centerOnGps,
|
||||
toggleTrail,
|
||||
setNativeGpsPosition,
|
||||
setGpsError
|
||||
};
|
||||
|
||||
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", {
|
||||
maxZoom: 22,
|
||||
attribution: "© OpenStreetMap contributors"
|
||||
});
|
||||
|
||||
lmeState.baseLayers.satellite = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", {
|
||||
maxZoom: 22,
|
||||
attribution: "Tiles © Esri"
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
lmeState.map.on("click", async event => {
|
||||
const info = await queryFeatureInfo(event.latlng);
|
||||
if (info) {
|
||||
L.popup().setLatLng(event.latlng).setContent(info).openOn(lmeState.map);
|
||||
}
|
||||
});
|
||||
|
||||
setInfo("Mappa pronta: satellite + catasto AdE");
|
||||
}
|
||||
|
||||
async function search(text) {
|
||||
const query = (text || "").trim();
|
||||
if (!query) {
|
||||
setInfo("Inserisci una localita o coordinate.");
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinate = parseCoordinates(query);
|
||||
if (coordinate) {
|
||||
lmeState.map.setView(coordinate, 19);
|
||||
setInfo(`Coordinate: ${coordinate[0].toFixed(6)}, ${coordinate[1].toFixed(6)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(query)}`, {
|
||||
headers: { "Accept": "application/json" }
|
||||
});
|
||||
const results = await response.json();
|
||||
if (!results.length) {
|
||||
setInfo("Localita non trovata.");
|
||||
return;
|
||||
}
|
||||
|
||||
const place = results[0];
|
||||
lmeState.map.setView([Number(place.lat), Number(place.lon)], 18);
|
||||
setInfo(place.display_name);
|
||||
}
|
||||
|
||||
async function locateParcelArea(region, municipality, municipalityCode, sheet, parcel, label) {
|
||||
if (!sheet || !parcel || (!municipality && !municipalityCode)) {
|
||||
setInfo("Inserisci Comune o codice catastale, Foglio e Particella.");
|
||||
return;
|
||||
}
|
||||
|
||||
setInfo("Cerco la particella nell'indice catastale...");
|
||||
try {
|
||||
const match = await findParcelIndexPoint(region, municipality, municipalityCode, sheet, parcel);
|
||||
if (!match) {
|
||||
const query = [municipality, region, "Italia"].filter(Boolean).join(", ");
|
||||
await search(query);
|
||||
setInfo("Particella non trovata nell'indice. Ho aperto il Comune: usa tap/GPS per interrogare AdE.");
|
||||
return;
|
||||
}
|
||||
|
||||
const meters = 8;
|
||||
const deltaLat = meters / 111320;
|
||||
const deltaLon = meters / (111320 * Math.cos(match.lat * Math.PI / 180));
|
||||
const geojson = await fetchOfficialParcelsByBbox(match.lat - deltaLat, match.lon - deltaLon, match.lat + deltaLat, match.lon + deltaLon);
|
||||
const normalizedParcel = normalizeParcel(parcel);
|
||||
const feature = (geojson.features || []).find(item => {
|
||||
const props = item.properties || {};
|
||||
return String(props.LABEL || "").trim().toUpperCase() === normalizedParcel.toUpperCase()
|
||||
|| String(props.NATIONALCADASTRALREFERENCE || "").toUpperCase().endsWith(`.${normalizedParcel.toUpperCase()}`);
|
||||
}) || (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [match.lon, match.lat])) || geojson.features?.[0];
|
||||
|
||||
if (!feature) {
|
||||
lmeState.map.setView([match.lat, match.lon], 20);
|
||||
setInfo("Punto interno trovato, ma il WFS AdE non ha restituito il poligono in quel momento.");
|
||||
return;
|
||||
}
|
||||
|
||||
lmeState.currentParcelLayer.clearLayers();
|
||||
const layer = L.geoJSON(feature, {
|
||||
style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 }
|
||||
}).addTo(lmeState.currentParcelLayer);
|
||||
fitLayer(layer);
|
||||
|
||||
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || `${match.comune}_${match.foglio}.${match.particella}`;
|
||||
setText("parcelState", `Particella selezionata: ${ref}`);
|
||||
setInfo(`Trovata ${ref}. Ora puoi attivare il GPS e camminare dentro il confine evidenziato.`);
|
||||
} catch (error) {
|
||||
setInfo(`Ricerca catastale non completata: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function findParcelIndexPoint(region, municipality, municipalityCode, sheet, parcel) {
|
||||
const conn = await getDuckDbConnection();
|
||||
const code = (municipalityCode || "").trim().toUpperCase();
|
||||
const name = (municipality || "").trim().toUpperCase().replaceAll("'", "''");
|
||||
const normalizedSheet = normalizeSheet(sheet);
|
||||
const normalizedParcel = normalizeParcel(parcel).replaceAll("'", "''");
|
||||
|
||||
let indexWhere = "";
|
||||
if (code) {
|
||||
indexWhere = `comune = '${code.replaceAll("'", "''")}'`;
|
||||
} else {
|
||||
indexWhere = `upper(DENOMINAZIONE_IT) = '${name}'`;
|
||||
}
|
||||
|
||||
const indexUrl = "https://raw.githubusercontent.com/ondata/dati_catastali/main/S_0000_ITALIA/anagrafica/index.parquet";
|
||||
const indexRows = await queryDuckDb(conn, `
|
||||
SELECT comune, file, DENOMINAZIONE_IT
|
||||
FROM '${indexUrl}'
|
||||
WHERE ${indexWhere}
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
if (!indexRows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = chooseMunicipalityRow(indexRows, region);
|
||||
const dataUrl = `https://raw.githubusercontent.com/ondata/dati_catastali/main/S_0000_ITALIA/anagrafica/${row.file}`;
|
||||
const parcelRows = await queryDuckDb(conn, `
|
||||
SELECT INSPIREID_LOCALID, comune, foglio, particella, x, y
|
||||
FROM '${dataUrl}'
|
||||
WHERE comune = '${row.comune}'
|
||||
AND foglio = '${normalizedSheet}'
|
||||
AND upper(particella) = upper('${normalizedParcel}')
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
if (!parcelRows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parcelRow = parcelRows[0];
|
||||
return {
|
||||
inspireId: parcelRow.INSPIREID_LOCALID,
|
||||
comune: parcelRow.comune,
|
||||
foglio: parcelRow.foglio,
|
||||
particella: parcelRow.particella,
|
||||
lon: Number(parcelRow.x) / 1000000,
|
||||
lat: Number(parcelRow.y) / 1000000
|
||||
};
|
||||
}
|
||||
|
||||
function chooseMunicipalityRow(rows, region) {
|
||||
const wanted = (region || "").trim().toUpperCase();
|
||||
if (!wanted) {
|
||||
return rows[0];
|
||||
}
|
||||
return rows.find(row => String(row.file || "").toUpperCase().includes(wanted)) || rows[0];
|
||||
}
|
||||
|
||||
async function getDuckDbConnection() {
|
||||
if (lmeState.duckDbConnection) {
|
||||
return lmeState.duckDbConnection;
|
||||
}
|
||||
|
||||
const duckdb = await import("https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/+esm");
|
||||
const bundles = duckdb.getJsDelivrBundles();
|
||||
const bundle = await duckdb.selectBundle(bundles);
|
||||
const workerUrl = URL.createObjectURL(new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" }));
|
||||
const worker = new Worker(workerUrl);
|
||||
const logger = new duckdb.ConsoleLogger();
|
||||
const db = new duckdb.AsyncDuckDB(logger, worker);
|
||||
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
const conn = await db.connect();
|
||||
|
||||
try {
|
||||
await conn.query("INSTALL httpfs; LOAD httpfs;");
|
||||
} catch {
|
||||
// Some DuckDB-WASM bundles already include HTTP support.
|
||||
}
|
||||
|
||||
lmeState.duckDb = db;
|
||||
lmeState.duckDbConnection = conn;
|
||||
return conn;
|
||||
}
|
||||
|
||||
async function queryDuckDb(conn, sql) {
|
||||
const table = await conn.query(sql);
|
||||
return table.toArray().map(row => {
|
||||
if (typeof row.toJSON === "function") {
|
||||
return row.toJSON();
|
||||
}
|
||||
|
||||
const output = {};
|
||||
for (const key of Object.keys(row)) {
|
||||
output[key] = row[key];
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSheet(sheet) {
|
||||
const value = String(sheet || "").trim().toUpperCase();
|
||||
return /^\d+$/.test(value) ? value.padStart(4, "0") : value;
|
||||
}
|
||||
|
||||
function normalizeParcel(parcel) {
|
||||
return String(parcel || "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
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() {
|
||||
const input = document.getElementById("gisFileInput");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.addEventListener("change", async event => {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
lmeState.map.on(L.Draw.Event.CREATED, event => {
|
||||
lmeState.drawings.addLayer(event.layer);
|
||||
updateGpsInsideState();
|
||||
});
|
||||
setInfo("Disegno attivo: crea POI, linee e aree sulla 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.currentBase.bringToBack();
|
||||
}
|
||||
|
||||
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();
|
||||
updateOfficialParcelFromPoint(latitude, longitude, accuracy);
|
||||
}
|
||||
|
||||
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 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", "Fuori dai poligoni caricati");
|
||||
}
|
||||
|
||||
async function updateOfficialParcelFromPoint(latitude, longitude, accuracy) {
|
||||
const now = Date.now();
|
||||
if (lmeState.wfsLookupInFlight || now - lmeState.lastWfsLookupAt < 2500) {
|
||||
return;
|
||||
}
|
||||
|
||||
lmeState.wfsLookupInFlight = true;
|
||||
lmeState.lastWfsLookupAt = now;
|
||||
|
||||
const meters = Math.max(2, Math.min(Number(accuracy || 5), 25));
|
||||
const deltaLat = meters / 111320;
|
||||
const deltaLon = meters / (111320 * Math.cos(latitude * Math.PI / 180));
|
||||
try {
|
||||
const geojson = await fetchOfficialParcelsByBbox(latitude - deltaLat, longitude - deltaLon, latitude + deltaLat, longitude + deltaLon);
|
||||
const feature = (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [longitude, latitude])) || geojson.features?.[0];
|
||||
|
||||
if (!feature) {
|
||||
return;
|
||||
}
|
||||
|
||||
lmeState.currentParcelLayer.clearLayers();
|
||||
L.geoJSON(feature, {
|
||||
style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.24 }
|
||||
}).addTo(lmeState.currentParcelLayer);
|
||||
|
||||
const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || feature.properties?.LABEL || "particella AdE";
|
||||
setText("parcelState", `Dentro particella AdE: ${ref}`);
|
||||
} finally {
|
||||
lmeState.wfsLookupInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOfficialParcelsByBbox(south, west, north, east) {
|
||||
const url = `${wfsUrl()}language=ita&SERVICE=WFS&VERSION=2.0.0&TYPENAMES=CP:CadastralParcel&SRSNAME=urn:ogc:def:crs:EPSG::6706&BBOX=${south},${west},${north},${east}&REQUEST=GetFeature&COUNT=20`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return { type: "FeatureCollection", features: [] };
|
||||
}
|
||||
const xmlText = await response.text();
|
||||
return gmlParcelsToGeoJson(xmlText);
|
||||
}
|
||||
|
||||
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 parseCoordinates(text) {
|
||||
const parts = text.split(/[,\s;]+/).map(Number).filter(value => !Number.isNaN(value));
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const [lat, lng] = parts;
|
||||
if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) {
|
||||
return [lat, lng];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 wfsUrl() {
|
||||
return "https://wfs.cartografia.agenziaentrate.gov.it/inspire/wfs/owfs01.php?";
|
||||
}
|
||||
|
||||
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("'", "'");
|
||||
}
|
||||
Reference in new issue
Block a user