From 362e1fc88c7e014f6a939e9e6c0ed5cfc4289a2b0aaec460849cfb83ea78a9be Mon Sep 17 00:00:00 2001 From: K1 Date: Sun, 3 May 2026 18:34:12 +0200 Subject: [PATCH 1/3] map types Signed-off-by: K1 --- dve.css | 60 +++++++++++++ dve.js | 249 ++++++++++++++++++++++++++++++++++++++++++++--------- index.html | 18 ++-- 3 files changed, 275 insertions(+), 52 deletions(-) diff --git a/dve.css b/dve.css index 27874c9..64e9cdd 100644 --- a/dve.css +++ b/dve.css @@ -57,6 +57,66 @@ 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 4c99de8..ec95cc4 100644 --- a/dve.js +++ b/dve.js @@ -7,6 +7,11 @@ const json_files = [ 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; @@ -14,18 +19,151 @@ let activeMouseMap = null; let activeMousePoint = null; let activeMouseLatLng = null; -let TILE_SERVERS = []; +let TILE_SERVERS = {}; let maps = []; -let layers = []; +let baseLayers = []; +let overlayLayers = []; let crosshairs = []; -function fillDropdown(select) { - for (const name in TILE_SERVERS) { - const opt = document.createElement("option"); - opt.value = name; - opt.textContent = name; - select.appendChild(opt); - } +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 readUrlState() { @@ -57,23 +195,31 @@ function readUrlState() { } for (let i = 1; i <= state.count; i++) { - const layer = params.get("layer" + i); - - if (layer) state.layers.push(layer); + state.layers.push({ + base: params.get("layer" + i), + overlays: params.getAll("overlay" + i) + }); } return state; } -function getSelectedLayers(count) { - const selected = []; +function getLayerSelectorState(index) { + const selector = document.getElementById("select" + (parseInt(index, 10) + 1)); + const overlays = []; - for (let i = 1; i <= count; i++) { - const select = document.getElementById("select" + i); - if (select && select.value) selected.push(select.value); - } + selector.querySelectorAll(".layer-overlay-option input:checked").forEach(input => { + overlays.push(input.dataset.layer); + }); - return selected; + return { + base: selector.dataset.baseLayer, + overlays + }; +} + +function addUrlParam(parts, key, value) { + if (value) parts.push(key + "=" + encodeURIComponent(value)); } function writeUrlState(map) { @@ -82,15 +228,18 @@ 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); - hash += "&count=" + count; + parts.push("count=" + count); - const selected = getSelectedLayers(count); - selected.forEach((name, index) => { - hash += "&layer" + (index + 1) + "=" + encodeURIComponent(name); - }); + 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)); + } + hash += "&" + parts.join("&"); history.replaceState(null, "", hash); }, 100); } @@ -109,15 +258,13 @@ function sync(source) { updateCrosshairs(); } -function setLayer(map, config, currentLayer) { - if (currentLayer) map.removeLayer(currentLayer); - +function createLayer(config) { if (config.type === "xyz") { return L.tileLayer(config.url, { maxZoom: 19, tileSize: 256, referrerPolicy: "strict-origin-when-cross-origin" - }).addTo(map); + }); } if (config.type === "wms") { @@ -128,7 +275,28 @@ function setLayer(map, config, currentLayer) { version: config.version || "1.3.0", attribution: config.attribution || "", referrerPolicy: "strict-origin-when-cross-origin" - }).addTo(map); + }); + } +} + +function setLayer(map, config, currentLayer) { + if (currentLayer) map.removeLayer(currentLayer); + + return createLayer(config).addTo(map); +} + +function setBaseLayer(index, name) { + baseLayers[index] = setLayer(maps[index], TILE_SERVERS[name], baseLayers[index]); +} + +function setOverlayLayer(index, name, enabled) { + if (enabled) { + if (!overlayLayers[index][name]) { + overlayLayers[index][name] = createLayer(TILE_SERVERS[name]).addTo(maps[index]); + } + } else if (overlayLayers[index][name]) { + maps[index].removeLayer(overlayLayers[index][name]); + delete overlayLayers[index][name]; } } @@ -232,13 +400,11 @@ function init() { const mapCountSelector = document.getElementById("mapCountSelector"); fillMapCountDropdown(mapCountSelector); - for (let i = 1; i <= 9; i++) { - const select = document.getElementById("select" + i); - fillDropdown(select); + document.addEventListener("click", () => closeLayerSelectors()); - if (urlState.layers[i - 1] && TILE_SERVERS[urlState.layers[i - 1]]) { - select.value = urlState.layers[i - 1]; - } + for (let i = 1; i <= 9; i++) { + const selector = document.getElementById("select" + i); + fillLayerSelector(selector, i - 1, urlState.layers[i - 1] || {}); const map = L.map("map" + i, { center: urlState.center, zoom: urlState.zoom, zoomControl: false }); map.on("move", () => sync(map)); @@ -247,17 +413,14 @@ function init() { L.control.zoom({ position: "topright" }).addTo(map); maps.push(map); - layers.push(null); + baseLayers.push(null); + overlayLayers.push({}); crosshairs.push(createCrosshair(map)); initCrosshair(map); - 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); - }); + const layerState = getLayerSelectorState(i - 1); + setBaseLayer(i - 1, layerState.base); + layerState.overlays.forEach(name => setOverlayLayer(i - 1, name, true)); } mapCountSelector.value = urlState.count; diff --git a/index.html b/index.html index e535ec3..04ec9a8 100644 --- a/index.html +++ b/index.html @@ -31,39 +31,39 @@
- +
- +
From 83dd1fd3fe9c62ca01a4ad81bc5081073a46f2611eccbd529dc3c530194a7839 Mon Sep 17 00:00:00 2001 From: K1 Date: Sun, 3 May 2026 18:40:52 +0200 Subject: [PATCH 2/3] overlays fix to stay on top when base map changes Signed-off-by: K1 --- dve.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/dve.js b/dve.js index ec95cc4..76350fa 100644 --- a/dve.js +++ b/dve.js @@ -258,11 +258,12 @@ function sync(source) { updateCrosshairs(); } -function createLayer(config) { +function createLayer(config, zIndex) { if (config.type === "xyz") { return L.tileLayer(config.url, { maxZoom: 19, tileSize: 256, + zIndex, referrerPolicy: "strict-origin-when-cross-origin" }); } @@ -274,26 +275,37 @@ function createLayer(config) { 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) { +function setLayer(map, config, currentLayer, zIndex) { if (currentLayer) map.removeLayer(currentLayer); - return createLayer(config).addTo(map); + 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]); + 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]).addTo(maps[index]); + 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]; From 5dd3a13b3b24b8cbbff5e73b7a687a3f0ef5545f90013f10e30b7339fce396f0 Mon Sep 17 00:00:00 2001 From: K1 Date: Sun, 3 May 2026 20:01:23 +0200 Subject: [PATCH 3/3] new maps Signed-off-by: K1 --- dve.js | 3 +- tileconfig/osm.json | 48 +++++++++++++++++++++++ tileconfig/others.json | 89 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100755 tileconfig/others.json diff --git a/dve.js b/dve.js index 76350fa..0738ebe 100644 --- a/dve.js +++ b/dve.js @@ -1,7 +1,8 @@ const json_files = [ "tileconfig/osm.json", "tileconfig/mapycom.json", - "tileconfig/praha.json" + "tileconfig/praha.json", + "tileconfig/others.json" ]; const center = [50.08804, 14.42076]; diff --git a/tileconfig/osm.json b/tileconfig/osm.json index 5dd483e..36df021 100644 --- a/tileconfig/osm.json +++ b/tileconfig/osm.json @@ -2,5 +2,53 @@ "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 new file mode 100755 index 0000000..198ebe3 --- /dev/null +++ b/tileconfig/others.json @@ -0,0 +1,89 @@ +{ + "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" + } +}