map types

Signed-off-by: K1 <git@karoh.net>
This commit is contained in:
K1 2026-05-03 18:34:12 +02:00
parent e1b205d01c
commit 362e1fc88c
3 changed files with 275 additions and 52 deletions

60
dve.css
View file

@ -57,6 +57,66 @@ html, body {
top: auto; top: auto;
bottom: 10px; 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 { .crosshair {
position: absolute; position: absolute;

249
dve.js
View file

@ -7,6 +7,11 @@ const json_files = [
const center = [50.08804, 14.42076]; const center = [50.08804, 14.42076];
const zoom = 14; const zoom = 14;
const mapCounts = [1, 2, 4, 6, 9]; const mapCounts = [1, 2, 4, 6, 9];
const layerCategories = {
classic: "Klasické",
ortho: "Ortomapy",
overlay: "Overlaye"
};
let syncing = false; let syncing = false;
let urlUpdateTimer = null; let urlUpdateTimer = null;
@ -14,18 +19,151 @@ let activeMouseMap = null;
let activeMousePoint = null; let activeMousePoint = null;
let activeMouseLatLng = null; let activeMouseLatLng = null;
let TILE_SERVERS = []; let TILE_SERVERS = {};
let maps = []; let maps = [];
let layers = []; let baseLayers = [];
let overlayLayers = [];
let crosshairs = []; let crosshairs = [];
function fillDropdown(select) { function getLayerCategory(name, config) {
for (const name in TILE_SERVERS) { const category = (config.category || "").toString().toLowerCase();
const opt = document.createElement("option"); const lowerName = name.toLowerCase();
opt.value = name;
opt.textContent = name; if (["overlay", "overlays", "prekryv", "překryv"].includes(category)) return "overlay";
select.appendChild(opt); 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() { function readUrlState() {
@ -57,23 +195,31 @@ function readUrlState() {
} }
for (let i = 1; i <= state.count; i++) { for (let i = 1; i <= state.count; i++) {
const layer = params.get("layer" + i); state.layers.push({
base: params.get("layer" + i),
if (layer) state.layers.push(layer); overlays: params.getAll("overlay" + i)
});
} }
return state; return state;
} }
function getSelectedLayers(count) { function getLayerSelectorState(index) {
const selected = []; const selector = document.getElementById("select" + (parseInt(index, 10) + 1));
const overlays = [];
for (let i = 1; i <= count; i++) { selector.querySelectorAll(".layer-overlay-option input:checked").forEach(input => {
const select = document.getElementById("select" + i); overlays.push(input.dataset.layer);
if (select && select.value) selected.push(select.value); });
}
return selected; return {
base: selector.dataset.baseLayer,
overlays
};
}
function addUrlParam(parts, key, value) {
if (value) parts.push(key + "=" + encodeURIComponent(value));
} }
function writeUrlState(map) { function writeUrlState(map) {
@ -82,15 +228,18 @@ function writeUrlState(map) {
urlUpdateTimer = setTimeout(() => { urlUpdateTimer = setTimeout(() => {
const c = map.getCenter(); const c = map.getCenter();
const count = parseInt(document.getElementById("mapCountSelector").value, 10); const count = parseInt(document.getElementById("mapCountSelector").value, 10);
const parts = [];
let hash = "#map=" + map.getZoom() + "/" + c.lat.toFixed(5) + "/" + c.lng.toFixed(5); let hash = "#map=" + map.getZoom() + "/" + c.lat.toFixed(5) + "/" + c.lng.toFixed(5);
hash += "&count=" + count; parts.push("count=" + count);
const selected = getSelectedLayers(count); for (let i = 0; i < count; i++) {
selected.forEach((name, index) => { const state = getLayerSelectorState(i);
hash += "&layer" + (index + 1) + "=" + encodeURIComponent(name); addUrlParam(parts, "layer" + (i + 1), state.base);
}); state.overlays.forEach(name => addUrlParam(parts, "overlay" + (i + 1), name));
}
hash += "&" + parts.join("&");
history.replaceState(null, "", hash); history.replaceState(null, "", hash);
}, 100); }, 100);
} }
@ -109,15 +258,13 @@ function sync(source) {
updateCrosshairs(); updateCrosshairs();
} }
function setLayer(map, config, currentLayer) { function createLayer(config) {
if (currentLayer) map.removeLayer(currentLayer);
if (config.type === "xyz") { if (config.type === "xyz") {
return L.tileLayer(config.url, { return L.tileLayer(config.url, {
maxZoom: 19, maxZoom: 19,
tileSize: 256, tileSize: 256,
referrerPolicy: "strict-origin-when-cross-origin" referrerPolicy: "strict-origin-when-cross-origin"
}).addTo(map); });
} }
if (config.type === "wms") { if (config.type === "wms") {
@ -128,7 +275,28 @@ function setLayer(map, config, currentLayer) {
version: config.version || "1.3.0", version: config.version || "1.3.0",
attribution: config.attribution || "", attribution: config.attribution || "",
referrerPolicy: "strict-origin-when-cross-origin" 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"); const mapCountSelector = document.getElementById("mapCountSelector");
fillMapCountDropdown(mapCountSelector); fillMapCountDropdown(mapCountSelector);
for (let i = 1; i <= 9; i++) { document.addEventListener("click", () => closeLayerSelectors());
const select = document.getElementById("select" + i);
fillDropdown(select);
if (urlState.layers[i - 1] && TILE_SERVERS[urlState.layers[i - 1]]) { for (let i = 1; i <= 9; i++) {
select.value = urlState.layers[i - 1]; 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 }); const map = L.map("map" + i, { center: urlState.center, zoom: urlState.zoom, zoomControl: false });
map.on("move", () => sync(map)); map.on("move", () => sync(map));
@ -247,17 +413,14 @@ function init() {
L.control.zoom({ position: "topright" }).addTo(map); L.control.zoom({ position: "topright" }).addTo(map);
maps.push(map); maps.push(map);
layers.push(null); baseLayers.push(null);
overlayLayers.push({});
crosshairs.push(createCrosshair(map)); crosshairs.push(createCrosshair(map));
initCrosshair(map); initCrosshair(map);
layers[i - 1] = setLayer(map, TILE_SERVERS[select.value], layers[i - 1]); const layerState = getLayerSelectorState(i - 1);
setBaseLayer(i - 1, layerState.base);
select.addEventListener("change", e => { layerState.overlays.forEach(name => setOverlayLayer(i - 1, name, true));
const key = e.target.value;
layers[i - 1] = setLayer(map, TILE_SERVERS[key], layers[i - 1]);
writeUrlState(map);
});
} }
mapCountSelector.value = urlState.count; mapCountSelector.value = urlState.count;

View file

@ -31,39 +31,39 @@
<body> <body>
<div id="container" data-count="2"> <div id="container" data-count="2">
<div class="map-wrapper"> <div class="map-wrapper">
<select id="select1" class="selector"></select> <div id="select1" class="selector layer-selector"></div>
<div id="map1" class="map"></div> <div id="map1" class="map"></div>
</div> </div>
<div class="map-wrapper"> <div class="map-wrapper">
<select id="select2" class="selector"></select> <div id="select2" class="selector layer-selector"></div>
<div id="map2" class="map"></div> <div id="map2" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select3" class="selector"></select> <div id="select3" class="selector layer-selector"></div>
<div id="map3" class="map"></div> <div id="map3" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select4" class="selector"></select> <div id="select4" class="selector layer-selector"></div>
<div id="map4" class="map"></div> <div id="map4" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select5" class="selector"></select> <div id="select5" class="selector layer-selector"></div>
<div id="map5" class="map"></div> <div id="map5" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select6" class="selector"></select> <div id="select6" class="selector layer-selector"></div>
<div id="map6" class="map"></div> <div id="map6" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select7" class="selector"></select> <div id="select7" class="selector layer-selector"></div>
<div id="map7" class="map"></div> <div id="map7" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select8" class="selector"></select> <div id="select8" class="selector layer-selector"></div>
<div id="map8" class="map"></div> <div id="map8" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<select id="select9" class="selector"></select> <div id="select9" class="selector layer-selector"></div>
<div id="map9" class="map"></div> <div id="map9" class="map"></div>
</div> </div>
</div> </div>