Compare commits

..

No commits in common. "5dd3a13b3b24b8cbbff5e73b7a687a3f0ef5545f90013f10e30b7339fce396f0" and "e1b205d01c478797038e1c9a35605852dfb52040e7d13445a480bf7641f6f20c" have entirely different histories.

5 changed files with 53 additions and 426 deletions

60
dve.css
View file

@ -57,66 +57,6 @@ 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;

262
dve.js
View file

@ -1,18 +1,12 @@
const json_files = [ const json_files = [
"tileconfig/osm.json", "tileconfig/osm.json",
"tileconfig/mapycom.json", "tileconfig/mapycom.json",
"tileconfig/praha.json", "tileconfig/praha.json"
"tileconfig/others.json"
]; ];
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;
@ -20,151 +14,18 @@ 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 baseLayers = []; let layers = [];
let overlayLayers = [];
let crosshairs = []; let crosshairs = [];
function getLayerCategory(name, config) { function fillDropdown(select) {
const category = (config.category || "").toString().toLowerCase(); for (const name in TILE_SERVERS) {
const lowerName = name.toLowerCase(); const opt = document.createElement("option");
opt.value = name;
if (["overlay", "overlays", "prekryv", "překryv"].includes(category)) return "overlay"; opt.textContent = name;
if (["ortho", "ortomap", "orthomap", "orthophoto", "ortofoto", "ortomapa"].includes(category)) return "ortho"; select.appendChild(opt);
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() {
@ -196,31 +57,23 @@ function readUrlState() {
} }
for (let i = 1; i <= state.count; i++) { for (let i = 1; i <= state.count; i++) {
state.layers.push({ const layer = params.get("layer" + i);
base: params.get("layer" + i),
overlays: params.getAll("overlay" + i) if (layer) state.layers.push(layer);
});
} }
return state; return state;
} }
function getLayerSelectorState(index) { function getSelectedLayers(count) {
const selector = document.getElementById("select" + (parseInt(index, 10) + 1)); const selected = [];
const overlays = [];
selector.querySelectorAll(".layer-overlay-option input:checked").forEach(input => { for (let i = 1; i <= count; i++) {
overlays.push(input.dataset.layer); const select = document.getElementById("select" + i);
}); if (select && select.value) selected.push(select.value);
}
return { return selected;
base: selector.dataset.baseLayer,
overlays
};
}
function addUrlParam(parts, key, value) {
if (value) parts.push(key + "=" + encodeURIComponent(value));
} }
function writeUrlState(map) { function writeUrlState(map) {
@ -229,18 +82,15 @@ 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);
parts.push("count=" + count); hash += "&count=" + count;
for (let i = 0; i < count; i++) { const selected = getSelectedLayers(count);
const state = getLayerSelectorState(i); selected.forEach((name, index) => {
addUrlParam(parts, "layer" + (i + 1), state.base); hash += "&layer" + (index + 1) + "=" + encodeURIComponent(name);
state.overlays.forEach(name => addUrlParam(parts, "overlay" + (i + 1), name)); });
}
hash += "&" + parts.join("&");
history.replaceState(null, "", hash); history.replaceState(null, "", hash);
}, 100); }, 100);
} }
@ -259,14 +109,15 @@ function sync(source) {
updateCrosshairs(); updateCrosshairs();
} }
function createLayer(config, zIndex) { function setLayer(map, config, currentLayer) {
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,
zIndex,
referrerPolicy: "strict-origin-when-cross-origin" referrerPolicy: "strict-origin-when-cross-origin"
}); }).addTo(map);
} }
if (config.type === "wms") { if (config.type === "wms") {
@ -276,40 +127,8 @@ function createLayer(config, zIndex) {
transparent: config.transparent ?? true, transparent: config.transparent ?? true,
version: config.version || "1.3.0", version: config.version || "1.3.0",
attribution: config.attribution || "", attribution: config.attribution || "",
zIndex,
referrerPolicy: "strict-origin-when-cross-origin" referrerPolicy: "strict-origin-when-cross-origin"
}); }).addTo(map);
}
}
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];
} }
} }
@ -413,11 +232,13 @@ function init() {
const mapCountSelector = document.getElementById("mapCountSelector"); const mapCountSelector = document.getElementById("mapCountSelector");
fillMapCountDropdown(mapCountSelector); fillMapCountDropdown(mapCountSelector);
document.addEventListener("click", () => closeLayerSelectors());
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
const selector = document.getElementById("select" + i); const select = document.getElementById("select" + i);
fillLayerSelector(selector, i - 1, urlState.layers[i - 1] || {}); 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 }); const map = L.map("map" + i, { center: urlState.center, zoom: urlState.zoom, zoomControl: false });
map.on("move", () => sync(map)); map.on("move", () => sync(map));
@ -426,14 +247,17 @@ function init() {
L.control.zoom({ position: "topright" }).addTo(map); L.control.zoom({ position: "topright" }).addTo(map);
maps.push(map); maps.push(map);
baseLayers.push(null); layers.push(null);
overlayLayers.push({});
crosshairs.push(createCrosshair(map)); crosshairs.push(createCrosshair(map));
initCrosshair(map); initCrosshair(map);
const layerState = getLayerSelectorState(i - 1); layers[i - 1] = setLayer(map, TILE_SERVERS[select.value], layers[i - 1]);
setBaseLayer(i - 1, layerState.base);
layerState.overlays.forEach(name => setOverlayLayer(i - 1, name, true)); 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; 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">
<div id="select1" class="selector layer-selector"></div> <select id="select1" class="selector"></select>
<div id="map1" class="map"></div> <div id="map1" class="map"></div>
</div> </div>
<div class="map-wrapper"> <div class="map-wrapper">
<div id="select2" class="selector layer-selector"></div> <select id="select2" class="selector"></select>
<div id="map2" class="map"></div> <div id="map2" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select3" class="selector layer-selector"></div> <select id="select3" class="selector"></select>
<div id="map3" class="map"></div> <div id="map3" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select4" class="selector layer-selector"></div> <select id="select4" class="selector"></select>
<div id="map4" class="map"></div> <div id="map4" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select5" class="selector layer-selector"></div> <select id="select5" class="selector"></select>
<div id="map5" class="map"></div> <div id="map5" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select6" class="selector layer-selector"></div> <select id="select6" class="selector"></select>
<div id="map6" class="map"></div> <div id="map6" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select7" class="selector layer-selector"></div> <select id="select7" class="selector"></select>
<div id="map7" class="map"></div> <div id="map7" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select8" class="selector layer-selector"></div> <select id="select8" class="selector"></select>
<div id="map8" class="map"></div> <div id="map8" class="map"></div>
</div> </div>
<div class="map-wrapper hidden"> <div class="map-wrapper hidden">
<div id="select9" class="selector layer-selector"></div> <select id="select9" class="selector"></select>
<div id="map9" class="map"></div> <div id="map9" class="map"></div>
</div> </div>
</div> </div>

View file

@ -2,53 +2,5 @@
"OpenStreetMap": { "OpenStreetMap": {
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"type": "xyz" "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"
} }
} }

View file

@ -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"
}
}