548 lines
12 KiB
JavaScript
548 lines
12 KiB
JavaScript
// public/js/mapBaseLayers.js
|
|
// Basemap 2D Leaflet e selettore livelli.
|
|
|
|
(() => {
|
|
"use strict";
|
|
|
|
const STORAGE_KEY = "selectedMapStyle";
|
|
const DEFAULT_STYLE = "osm";
|
|
|
|
let currentLayer = null;
|
|
let layers = {};
|
|
let layerControl = null;
|
|
let initializedMap = null;
|
|
let terrainPlaceholderLayer = null;
|
|
let terrainStyleControl = null;
|
|
let styleChangeInProgress = false;
|
|
|
|
function getMapboxToken() {
|
|
return window.AppConfig?.mapboxToken || "";
|
|
}
|
|
|
|
function createOsmLayer() {
|
|
return L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
subdomains: "abc",
|
|
maxZoom: 19,
|
|
attribution: "© OpenStreetMap contributors",
|
|
});
|
|
}
|
|
|
|
function createOpenTopoLayer() {
|
|
return L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
|
|
subdomains: "abc",
|
|
maxZoom: 17,
|
|
attribution:
|
|
"Map data: © OpenStreetMap contributors, SRTM | " +
|
|
"Map style: © OpenTopoMap",
|
|
});
|
|
}
|
|
|
|
function createMapboxLayer(styleId, token) {
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
|
|
const encodedToken = encodeURIComponent(token);
|
|
|
|
const tileUrl =
|
|
`https://api.mapbox.com/styles/v1/mapbox/${styleId}` +
|
|
`/tiles/512/{z}/{x}/{y}@2x` +
|
|
`?access_token=${encodedToken}`;
|
|
|
|
return L.tileLayer(tileUrl, {
|
|
tileSize: 512,
|
|
zoomOffset: -1,
|
|
maxZoom: 22,
|
|
attribution: "© Mapbox © OpenStreetMap contributors",
|
|
});
|
|
}
|
|
|
|
function createLayers() {
|
|
const token = getMapboxToken();
|
|
|
|
const result = {
|
|
osm: {
|
|
label: "OpenStreetMap",
|
|
layer: createOsmLayer(),
|
|
engine: "leaflet",
|
|
},
|
|
|
|
opentopo: {
|
|
label: "Topografica",
|
|
layer: createOpenTopoLayer(),
|
|
engine: "leaflet",
|
|
},
|
|
};
|
|
|
|
if (!token) {
|
|
console.warn(
|
|
"[mapBaseLayers] Token Mapbox non configurato. " +
|
|
"Saranno disponibili solamente OpenStreetMap " +
|
|
"e OpenTopoMap.",
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
const mapboxDefinitions = {
|
|
mapboxStreets: {
|
|
label: "Mapbox Streets",
|
|
styleId: "streets-v12",
|
|
},
|
|
|
|
mapboxOutdoors: {
|
|
label: "Mapbox Outdoors",
|
|
styleId: "outdoors-v12",
|
|
},
|
|
|
|
mapboxSatellite: {
|
|
label: "Mapbox Satellite",
|
|
styleId: "satellite-v9",
|
|
},
|
|
|
|
mapboxSatelliteStreets: {
|
|
label: "Mapbox Satellite + strade",
|
|
styleId: "satellite-streets-v12",
|
|
},
|
|
};
|
|
|
|
for (const [id, definition] of Object.entries(mapboxDefinitions)) {
|
|
const layer = createMapboxLayer(definition.styleId, token);
|
|
|
|
if (!layer) {
|
|
continue;
|
|
}
|
|
|
|
result[id] = {
|
|
label: definition.label,
|
|
layer,
|
|
};
|
|
}
|
|
|
|
terrainPlaceholderLayer = L.layerGroup();
|
|
|
|
result.terrain3d = {
|
|
label: "Terrain 3D",
|
|
layer: terrainPlaceholderLayer,
|
|
engine: "mapbox",
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
function getSavedStyle() {
|
|
const savedStyle = localStorage.getItem(STORAGE_KEY);
|
|
|
|
if (savedStyle && layers[savedStyle]) {
|
|
return savedStyle;
|
|
}
|
|
|
|
return DEFAULT_STYLE;
|
|
}
|
|
|
|
function findStyleIdByLayer(selectedLayer) {
|
|
for (const [styleId, definition] of Object.entries(layers)) {
|
|
if (definition.layer === selectedLayer) {
|
|
return styleId;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function dispatchStyleChange(styleId, label) {
|
|
const definition = layers[styleId];
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent("mapstylechange", {
|
|
detail: {
|
|
engine: definition?.engine || "leaflet",
|
|
|
|
styleId,
|
|
label,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function setStyle(map, styleId, save = true) {
|
|
if (!map) {
|
|
console.error("[mapBaseLayers] Istanza Leaflet mancante");
|
|
|
|
return false;
|
|
}
|
|
|
|
if (styleChangeInProgress) {
|
|
return false;
|
|
}
|
|
|
|
const selectedStyle = layers[styleId] ? styleId : DEFAULT_STYLE;
|
|
|
|
const definition = layers[selectedStyle];
|
|
|
|
if (!definition) {
|
|
console.error("[mapBaseLayers] Basemap non trovata:", styleId);
|
|
|
|
return false;
|
|
}
|
|
|
|
styleChangeInProgress = true;
|
|
|
|
try {
|
|
// ===============================
|
|
// TERRAIN 3D
|
|
// ===============================
|
|
|
|
if (selectedStyle === "terrain3d") {
|
|
const success = await window.MapTerrain3D?.enable?.();
|
|
|
|
if (!success) {
|
|
console.warn("[mapBaseLayers] Terrain 3D non attivato");
|
|
|
|
return false;
|
|
}
|
|
|
|
currentLayer = terrainPlaceholderLayer;
|
|
|
|
if (save) {
|
|
localStorage.setItem(STORAGE_KEY, selectedStyle);
|
|
}
|
|
|
|
syncTerrainStyleControl(selectedStyle);
|
|
|
|
dispatchStyleChange(selectedStyle, definition.label);
|
|
|
|
return true;
|
|
}
|
|
|
|
// ===============================
|
|
// MAPPA LEAFLET
|
|
// ===============================
|
|
|
|
if (window.MapTerrain3D?.enabled?.()) {
|
|
window.MapTerrain3D?.disable?.();
|
|
}
|
|
|
|
if (
|
|
currentLayer &&
|
|
currentLayer !== terrainPlaceholderLayer &&
|
|
map.hasLayer(currentLayer)
|
|
) {
|
|
map.removeLayer(currentLayer);
|
|
}
|
|
|
|
if (terrainPlaceholderLayer && map.hasLayer(terrainPlaceholderLayer)) {
|
|
map.removeLayer(terrainPlaceholderLayer);
|
|
}
|
|
|
|
currentLayer = definition.layer;
|
|
|
|
if (currentLayer && !map.hasLayer(currentLayer)) {
|
|
currentLayer.addTo(map);
|
|
}
|
|
|
|
if (save) {
|
|
localStorage.setItem(STORAGE_KEY, selectedStyle);
|
|
}
|
|
|
|
syncTerrainStyleControl(selectedStyle);
|
|
|
|
dispatchStyleChange(selectedStyle, definition.label);
|
|
|
|
return true;
|
|
} finally {
|
|
styleChangeInProgress = false;
|
|
}
|
|
}
|
|
|
|
function createControl(map) {
|
|
if (layerControl) {
|
|
map.removeControl(layerControl);
|
|
layerControl = null;
|
|
}
|
|
|
|
const baseLayers = {};
|
|
|
|
for (const definition of Object.values(layers)) {
|
|
baseLayers[definition.label] = definition.layer;
|
|
}
|
|
|
|
layerControl = L.control.layers(baseLayers, null, {
|
|
position: "topright",
|
|
collapsed: true,
|
|
});
|
|
|
|
layerControl.addTo(map);
|
|
|
|
map.on("baselayerchange", handleBaseLayerChange);
|
|
}
|
|
|
|
async function handleBaseLayerChange(event) {
|
|
const styleId = findStyleIdByLayer(event.layer);
|
|
|
|
if (!styleId) {
|
|
return;
|
|
}
|
|
|
|
// ===============================
|
|
// TERRAIN 3D
|
|
// ===============================
|
|
|
|
if (styleId === "terrain3d") {
|
|
const success = await setStyle(initializedMap, "terrain3d", true);
|
|
|
|
if (!success) {
|
|
await setStyle(initializedMap, DEFAULT_STYLE, true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// ===============================
|
|
// LEAFLET
|
|
// ===============================
|
|
|
|
currentLayer = event.layer;
|
|
|
|
if (window.MapTerrain3D?.enabled?.()) {
|
|
window.MapTerrain3D?.disable?.();
|
|
}
|
|
|
|
localStorage.setItem(STORAGE_KEY, styleId);
|
|
|
|
syncTerrainStyleControl(styleId);
|
|
|
|
dispatchStyleChange(styleId, event.name);
|
|
}
|
|
|
|
async function waitForConfiguration() {
|
|
if (window.AppConfigPromise) {
|
|
try {
|
|
await window.AppConfigPromise;
|
|
} catch (error) {
|
|
console.error("[mapBaseLayers] Errore configurazione:", error);
|
|
}
|
|
}
|
|
|
|
return window.AppConfig || {};
|
|
}
|
|
|
|
function getSelectedStyle() {
|
|
if (window.MapTerrain3D?.enabled?.()) {
|
|
return "terrain3d";
|
|
}
|
|
|
|
return findStyleIdByLayer(currentLayer) || DEFAULT_STYLE;
|
|
}
|
|
|
|
function syncTerrainStyleControl(styleId) {
|
|
const select = terrainStyleControl?.querySelector?.("select");
|
|
|
|
if (!select) {
|
|
return;
|
|
}
|
|
|
|
if (layers[styleId]) {
|
|
select.value = styleId;
|
|
}
|
|
}
|
|
|
|
function removeTerrainStyleControl() {
|
|
if (!terrainStyleControl) {
|
|
return;
|
|
}
|
|
|
|
terrainStyleControl.remove();
|
|
terrainStyleControl = null;
|
|
}
|
|
|
|
function mountTerrainStyleControl(targetContainer) {
|
|
if (!targetContainer) {
|
|
return;
|
|
}
|
|
|
|
removeTerrainStyleControl();
|
|
|
|
const wrapper = document.createElement("div");
|
|
|
|
wrapper.className = "terrain-style-control";
|
|
|
|
const select = document.createElement("select");
|
|
|
|
select.className = "terrain-style-select";
|
|
|
|
select.setAttribute("aria-label", "Tipo di mappa");
|
|
|
|
for (const [id, definition] of Object.entries(layers)) {
|
|
const option = document.createElement("option");
|
|
|
|
option.value = id;
|
|
option.textContent = definition.label;
|
|
|
|
select.appendChild(option);
|
|
}
|
|
|
|
select.value = getSelectedStyle();
|
|
|
|
select.addEventListener("change", async () => {
|
|
const selectedStyle = select.value;
|
|
|
|
const success = await setStyle(initializedMap, selectedStyle, true);
|
|
|
|
if (!success) {
|
|
select.value = getSelectedStyle();
|
|
}
|
|
});
|
|
|
|
wrapper.appendChild(select);
|
|
|
|
targetContainer.appendChild(wrapper);
|
|
|
|
terrainStyleControl = wrapper;
|
|
}
|
|
|
|
async function initialize(map) {
|
|
if (!map) {
|
|
console.error("[mapBaseLayers] Istanza mappa non disponibile");
|
|
|
|
return false;
|
|
}
|
|
|
|
if (typeof window.L === "undefined") {
|
|
console.error("[mapBaseLayers] Leaflet non disponibile");
|
|
|
|
return false;
|
|
}
|
|
|
|
await waitForConfiguration();
|
|
|
|
/*
|
|
* Evita una doppia inizializzazione sulla stessa
|
|
* istanza Leaflet.
|
|
*/
|
|
if (initializedMap === map && Object.keys(layers).length > 0) {
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Se il modulo viene inizializzato su una nuova mappa,
|
|
* rimuove il listener dalla precedente.
|
|
*/
|
|
if (initializedMap && initializedMap !== map) {
|
|
initializedMap.off("baselayerchange", handleBaseLayerChange);
|
|
}
|
|
|
|
initializedMap = map;
|
|
layers = createLayers();
|
|
|
|
const initialStyle = getSavedStyle();
|
|
|
|
const safeInitialStyle =
|
|
initialStyle === "terrain3d" ? DEFAULT_STYLE : initialStyle;
|
|
|
|
const styleApplied = await setStyle(map, safeInitialStyle, false);
|
|
|
|
createControl(map);
|
|
|
|
console.log("[mapBaseLayers] Basemap inizializzate", {
|
|
currentStyle: getCurrentStyle(),
|
|
availableStyles: getAvailableStyles().map((style) => style.id),
|
|
mapboxConfigured: Boolean(getMapboxToken()),
|
|
});
|
|
|
|
return styleApplied;
|
|
}
|
|
|
|
function getCurrentStyle() {
|
|
if (window.MapTerrain3D?.enabled?.()) {
|
|
return "terrain3d";
|
|
}
|
|
|
|
return findStyleIdByLayer(currentLayer) || DEFAULT_STYLE;
|
|
}
|
|
|
|
function getAvailableStyles() {
|
|
return Object.entries(layers).map(([id, definition]) => ({
|
|
id,
|
|
label: definition.label,
|
|
}));
|
|
}
|
|
|
|
function createLeafletLayer(
|
|
styleId,
|
|
) {
|
|
const token =
|
|
getMapboxToken();
|
|
|
|
switch (styleId) {
|
|
case "osm":
|
|
return createOsmLayer();
|
|
|
|
case "opentopo":
|
|
return createOpenTopoLayer();
|
|
|
|
case "mapboxStreets":
|
|
return createMapboxLayer(
|
|
"streets-v12",
|
|
token,
|
|
);
|
|
|
|
case "mapboxOutdoors":
|
|
return createMapboxLayer(
|
|
"outdoors-v12",
|
|
token,
|
|
);
|
|
|
|
case "mapboxSatellite":
|
|
return createMapboxLayer(
|
|
"satellite-v9",
|
|
token,
|
|
);
|
|
|
|
case "mapboxSatelliteStreets":
|
|
return createMapboxLayer(
|
|
"satellite-streets-v12",
|
|
token,
|
|
);
|
|
|
|
default:
|
|
return createOsmLayer();
|
|
}
|
|
}
|
|
|
|
function getMapboxStyleUrl(
|
|
styleId,
|
|
) {
|
|
switch (styleId) {
|
|
case "mapboxStreets":
|
|
return "mapbox://styles/mapbox/streets-v12";
|
|
|
|
case "mapboxSatellite":
|
|
return "mapbox://styles/mapbox/satellite-v9";
|
|
|
|
case "mapboxSatelliteStreets":
|
|
return "mapbox://styles/mapbox/satellite-streets-v12";
|
|
|
|
case "terrain3d":
|
|
case "mapboxOutdoors":
|
|
default:
|
|
return "mapbox://styles/mapbox/outdoors-v12";
|
|
}
|
|
}
|
|
|
|
window.MapBaseLayers =
|
|
Object.freeze({
|
|
initialize,
|
|
setStyle,
|
|
|
|
getCurrentStyle,
|
|
getAvailableStyles,
|
|
|
|
createLeafletLayer,
|
|
getMapboxStyleUrl,
|
|
|
|
mountTerrainStyleControl,
|
|
removeTerrainStyleControl,
|
|
});
|
|
|
|
})();
|