diff --git a/dve.css b/dve.css index 64e9cdd..27874c9 100644 --- a/dve.css +++ b/dve.css @@ -57,66 +57,6 @@ html, body { top: auto; bottom: 10px; } -.layer-selector { - padding: 0; - min-width: 190px; -} -.layer-selector-button { - width: 100%; - max-width: 280px; - border: 0; - background: transparent; - padding: 6px 24px 6px 8px; - text-align: left; - font: inherit; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.layer-selector-button::after { - content: "▾"; - position: absolute; - right: 8px; -} -.layer-selector-menu { - display: none; - min-width: 260px; - max-width: 360px; - max-height: 65vh; - overflow: auto; - border-top: 1px solid rgba(0,0,0,0.15); -} -.layer-selector.open .layer-selector-menu { - display: block; -} -.layer-group-title { - padding: 8px 8px 4px; - font-weight: bold; - color: #555; -} -.layer-option, -.layer-overlay-option { - display: block; - box-sizing: border-box; - width: 100%; - border: 0; - background: transparent; - padding: 5px 8px 5px 18px; - text-align: left; - font: inherit; - cursor: pointer; -} -.layer-option:hover, -.layer-overlay-option:hover { - background: rgba(0,0,0,0.08); -} -.layer-option.active { - font-weight: bold; -} -.layer-overlay-option input { - margin-right: 6px; -} .crosshair { position: absolute; diff --git a/dve.js b/dve.js index 0738ebe..4c99de8 100644 --- a/dve.js +++ b/dve.js @@ -1,18 +1,12 @@ const json_files = [ "tileconfig/osm.json", "tileconfig/mapycom.json", - "tileconfig/praha.json", - "tileconfig/others.json" + "tileconfig/praha.json" ]; const center = [50.08804, 14.42076]; const zoom = 14; const mapCounts = [1, 2, 4, 6, 9]; -const layerCategories = { - classic: "Klasické", - ortho: "Ortomapy", - overlay: "Overlaye" -}; let syncing = false; let urlUpdateTimer = null; @@ -20,151 +14,18 @@ let activeMouseMap = null; let activeMousePoint = null; let activeMouseLatLng = null; -let TILE_SERVERS = {}; +let TILE_SERVERS = []; let maps = []; -let baseLayers = []; -let overlayLayers = []; +let layers = []; let crosshairs = []; -function getLayerCategory(name, config) { - const category = (config.category || "").toString().toLowerCase(); - const lowerName = name.toLowerCase(); - - if (["overlay", "overlays", "prekryv", "překryv"].includes(category)) return "overlay"; - if (["ortho", "ortomap", "orthomap", "orthophoto", "ortofoto", "ortomapa"].includes(category)) return "ortho"; - if (["classic", "base", "normal", "klasicke", "klasické"].includes(category)) return "classic"; - - if (lowerName.includes("overlay")) return "overlay"; - if (lowerName.includes("orto")) return "ortho"; - - return "classic"; -} - -function getLayerNamesByCategory(category) { - return Object.keys(TILE_SERVERS).filter(name => getLayerCategory(name, TILE_SERVERS[name]) === category); -} - -function getBaseLayerNames() { - return Object.keys(TILE_SERVERS).filter(name => getLayerCategory(name, TILE_SERVERS[name]) !== "overlay"); -} - -function getValidBaseLayerName(name) { - if (name && TILE_SERVERS[name] && getLayerCategory(name, TILE_SERVERS[name]) !== "overlay") return name; - - return getBaseLayerNames()[0] || Object.keys(TILE_SERVERS)[0]; -} - -function getValidOverlayNames(names) { - const selected = []; - - names.forEach(name => { - if ( - !selected.includes(name) && - TILE_SERVERS[name] && - getLayerCategory(name, TILE_SERVERS[name]) === "overlay" - ) { - selected.push(name); - } - }); - - return selected; -} - -function closeLayerSelectors(except) { - document.querySelectorAll(".layer-selector.open").forEach(selector => { - if (selector !== except) selector.classList.remove("open"); - }); -} - -function updateLayerSelectorLabel(selector) { - const button = selector.querySelector(".layer-selector-button"); - const state = getLayerSelectorState(selector.dataset.index); - button.textContent = state.base + (state.overlays.length ? " +" + state.overlays.length : ""); - - selector.querySelectorAll(".layer-option").forEach(option => { - option.classList.toggle("active", option.dataset.layer === state.base); - }); -} - -function addLayerGroup(menu, title, names, overlay, selector) { - if (!names.length) return; - - const groupTitle = document.createElement("div"); - groupTitle.className = "layer-group-title"; - groupTitle.textContent = title; - menu.appendChild(groupTitle); - - names.forEach(name => { - if (overlay) { - const label = document.createElement("label"); - const input = document.createElement("input"); - label.className = "layer-overlay-option"; - input.type = "checkbox"; - input.dataset.layer = name; - input.checked = selector._initialOverlays.includes(name); - label.appendChild(input); - label.appendChild(document.createTextNode(name)); - menu.appendChild(label); - - input.addEventListener("change", e => { - const index = parseInt(selector.dataset.index, 10); - setOverlayLayer(index, name, e.target.checked); - updateLayerSelectorLabel(selector); - writeUrlState(maps[index]); - }); - } else { - const option = document.createElement("button"); - option.type = "button"; - option.className = "layer-option"; - option.dataset.layer = name; - option.textContent = name; - menu.appendChild(option); - - option.addEventListener("click", () => { - const index = parseInt(selector.dataset.index, 10); - selector.dataset.baseLayer = name; - setBaseLayer(index, name); - updateLayerSelectorLabel(selector); - selector.classList.remove("open"); - writeUrlState(maps[index]); - }); - } - }); -} - -function fillLayerSelector(selector, index, state) { - const baseLayer = getValidBaseLayerName(state.base); - const overlays = getValidOverlayNames(state.overlays || []); - - selector.innerHTML = ""; - selector.dataset.index = index; - selector.dataset.baseLayer = baseLayer; - selector._initialOverlays = overlays; - - const button = document.createElement("button"); - button.type = "button"; - button.className = "layer-selector-button"; - selector.appendChild(button); - - const menu = document.createElement("div"); - menu.className = "layer-selector-menu"; - selector.appendChild(menu); - - addLayerGroup(menu, layerCategories.classic, getLayerNamesByCategory("classic"), false, selector); - addLayerGroup(menu, layerCategories.ortho, getLayerNamesByCategory("ortho"), false, selector); - addLayerGroup(menu, layerCategories.overlay, getLayerNamesByCategory("overlay"), true, selector); - - ["click", "mousedown", "dblclick", "wheel", "touchstart"].forEach(type => { - selector.addEventListener(type, e => e.stopPropagation()); - }); - - button.addEventListener("click", () => { - const open = !selector.classList.contains("open"); - closeLayerSelectors(selector); - selector.classList.toggle("open", open); - }); - - updateLayerSelectorLabel(selector); +function fillDropdown(select) { + for (const name in TILE_SERVERS) { + const opt = document.createElement("option"); + opt.value = name; + opt.textContent = name; + select.appendChild(opt); + } } function readUrlState() { @@ -196,31 +57,23 @@ function readUrlState() { } for (let i = 1; i <= state.count; i++) { - state.layers.push({ - base: params.get("layer" + i), - overlays: params.getAll("overlay" + i) - }); + const layer = params.get("layer" + i); + + if (layer) state.layers.push(layer); } return state; } -function getLayerSelectorState(index) { - const selector = document.getElementById("select" + (parseInt(index, 10) + 1)); - const overlays = []; +function getSelectedLayers(count) { + const selected = []; - selector.querySelectorAll(".layer-overlay-option input:checked").forEach(input => { - overlays.push(input.dataset.layer); - }); + for (let i = 1; i <= count; i++) { + const select = document.getElementById("select" + i); + if (select && select.value) selected.push(select.value); + } - return { - base: selector.dataset.baseLayer, - overlays - }; -} - -function addUrlParam(parts, key, value) { - if (value) parts.push(key + "=" + encodeURIComponent(value)); + return selected; } function writeUrlState(map) { @@ -229,18 +82,15 @@ function writeUrlState(map) { urlUpdateTimer = setTimeout(() => { const c = map.getCenter(); const count = parseInt(document.getElementById("mapCountSelector").value, 10); - const parts = []; let hash = "#map=" + map.getZoom() + "/" + c.lat.toFixed(5) + "/" + c.lng.toFixed(5); - parts.push("count=" + count); + hash += "&count=" + count; - for (let i = 0; i < count; i++) { - const state = getLayerSelectorState(i); - addUrlParam(parts, "layer" + (i + 1), state.base); - state.overlays.forEach(name => addUrlParam(parts, "overlay" + (i + 1), name)); - } + const selected = getSelectedLayers(count); + selected.forEach((name, index) => { + hash += "&layer" + (index + 1) + "=" + encodeURIComponent(name); + }); - hash += "&" + parts.join("&"); history.replaceState(null, "", hash); }, 100); } @@ -259,14 +109,15 @@ function sync(source) { updateCrosshairs(); } -function createLayer(config, zIndex) { +function setLayer(map, config, currentLayer) { + if (currentLayer) map.removeLayer(currentLayer); + if (config.type === "xyz") { return L.tileLayer(config.url, { maxZoom: 19, tileSize: 256, - zIndex, referrerPolicy: "strict-origin-when-cross-origin" - }); + }).addTo(map); } if (config.type === "wms") { @@ -276,40 +127,8 @@ function createLayer(config, zIndex) { transparent: config.transparent ?? true, version: config.version || "1.3.0", attribution: config.attribution || "", - zIndex, referrerPolicy: "strict-origin-when-cross-origin" - }); - } -} - -function setLayer(map, config, currentLayer, zIndex) { - if (currentLayer) map.removeLayer(currentLayer); - - return createLayer(config, zIndex).addTo(map); -} - -function bringOverlayLayersToFront(index) { - Object.values(overlayLayers[index]).forEach(layer => { - layer.setZIndex(100); - layer.bringToFront(); - }); -} - -function setBaseLayer(index, name) { - baseLayers[index] = setLayer(maps[index], TILE_SERVERS[name], baseLayers[index], 1); - bringOverlayLayersToFront(index); -} - -function setOverlayLayer(index, name, enabled) { - if (enabled) { - if (!overlayLayers[index][name]) { - overlayLayers[index][name] = createLayer(TILE_SERVERS[name], 100).addTo(maps[index]); - } - - overlayLayers[index][name].bringToFront(); - } else if (overlayLayers[index][name]) { - maps[index].removeLayer(overlayLayers[index][name]); - delete overlayLayers[index][name]; + }).addTo(map); } } @@ -413,11 +232,13 @@ function init() { const mapCountSelector = document.getElementById("mapCountSelector"); fillMapCountDropdown(mapCountSelector); - document.addEventListener("click", () => closeLayerSelectors()); - for (let i = 1; i <= 9; i++) { - const selector = document.getElementById("select" + i); - fillLayerSelector(selector, i - 1, urlState.layers[i - 1] || {}); + const select = document.getElementById("select" + i); + fillDropdown(select); + + if (urlState.layers[i - 1] && TILE_SERVERS[urlState.layers[i - 1]]) { + select.value = urlState.layers[i - 1]; + } const map = L.map("map" + i, { center: urlState.center, zoom: urlState.zoom, zoomControl: false }); map.on("move", () => sync(map)); @@ -426,14 +247,17 @@ function init() { L.control.zoom({ position: "topright" }).addTo(map); maps.push(map); - baseLayers.push(null); - overlayLayers.push({}); + layers.push(null); crosshairs.push(createCrosshair(map)); initCrosshair(map); - const layerState = getLayerSelectorState(i - 1); - setBaseLayer(i - 1, layerState.base); - layerState.overlays.forEach(name => setOverlayLayer(i - 1, name, true)); + layers[i - 1] = setLayer(map, TILE_SERVERS[select.value], layers[i - 1]); + + select.addEventListener("change", e => { + const key = e.target.value; + layers[i - 1] = setLayer(map, TILE_SERVERS[key], layers[i - 1]); + writeUrlState(map); + }); } mapCountSelector.value = urlState.count; diff --git a/index.html b/index.html index 04ec9a8..e535ec3 100644 --- a/index.html +++ b/index.html @@ -31,39 +31,39 @@
-
+
-
+
diff --git a/tileconfig/osm.json b/tileconfig/osm.json index 36df021..5dd483e 100644 --- a/tileconfig/osm.json +++ b/tileconfig/osm.json @@ -2,53 +2,5 @@ "OpenStreetMap": { "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "type": "xyz" - }, - "OpenStreetMap DE": { - "url": "https://tile.openstreetmap.de/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenStreetMap HOT": { - "url": "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", - "type": "xyz" - }, - "CyclOSM Lite overlay": { - "url": "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenTopoMap": { - "url": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", - "type": "xyz" - }, - "ÖPNVKarte": { - "url": "https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenRailwayMap Standard overlay": { - "url": "https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenRailwayMap Maxspeed overlay": { - "url": "https://{s}.tiles.openrailwaymap.org/maxspeed/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenRailwayMap Signals overlay": { - "url": "https://{s}.tiles.openrailwaymap.org/signals/{z}/{x}/{y}.png", - "type": "xyz" - }, - "Waymarked Trails Hiking overlay": { - "url": "https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png", - "type": "xyz" - }, - "Waymarked Trails Cycling overlay": { - "url": "https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png", - "type": "xyz" - }, - "Waymarked Trails Riding overlay": { - "url": "https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png", - "type": "xyz" - }, - "OpenSeaMap Seamarks overlay": { - "url": "https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png", - "type": "xyz" } } diff --git a/tileconfig/others.json b/tileconfig/others.json deleted file mode 100755 index 198ebe3..0000000 --- a/tileconfig/others.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "CARTO Positron": { - "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", - "type": "xyz" - }, - "CARTO Dark Matter": { - "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", - "type": "xyz" - }, - "CARTO Voyager": { - "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}.png", - "type": "xyz" - }, - "Geoapify OSM Bright Smooth API key": { - "url": "https://maps.geoapify.com/v1/tile/osm-bright-smooth/{z}/{x}/{y}.png?apiKey=YOUR_GEOAPIFY_KEY", - "type": "xyz" - }, - "Lima Labs Carto demo": { - "url": "https://cdn.lima-labs.com/{z}/{x}/{y}.png?api=demo", - "type": "xyz" - }, - "Esri World Imagery": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", - "type": "xyz", - "category": "ortho" - }, - "Esri World Street Map": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri World Topo Map": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri NatGeo World Map": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri World Terrain": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri World Shaded Relief": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri World Physical": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri Ocean Basemap": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "Esri World Gray Canvas": { - "url": "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "USGS Topo": { - "url": "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "USGS Imagery Only": { - "url": "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}", - "type": "xyz", - "category": "ortho" - }, - "USGS Imagery Topo": { - "url": "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}", - "type": "xyz", - "category": "ortho" - }, - "USGS Shaded Relief": { - "url": "https://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "USGS Hydrography overlay": { - "url": "https://basemap.nationalmap.gov/arcgis/rest/services/USGSHydroCached/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "ČÚZK Ortofoto ČR Web Mercator": { - "url": "https://ags.cuzk.gov.cz/arcgis1/rest/services/ORTOFOTO_WM/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - }, - "ČÚZK Základní topografická mapa ČR Web Mercator": { - "url": "https://ags.cuzk.gov.cz/arcgis1/rest/services/ZTM_WM/MapServer/tile/{z}/{y}/{x}", - "type": "xyz" - } -}