const json_files = [ "tileconfig/osm.json", "tileconfig/mapycom.json", "tileconfig/praha.json" ]; const center = [50.08804, 14.42076]; const zoom = 14; const mapCounts = [2, 4, 6, 9]; let syncing = false; let urlUpdateTimer = null; let activeMouseMap = null; let activeMousePoint = null; let activeMouseLatLng = null; let TILE_SERVERS = []; let maps = []; let layers = []; 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 readUrlPosition() { const match = window.location.hash.match(/^#map=([0-9.]+)\/(-?[0-9.]+)\/(-?[0-9.]+)/); if (match) { const z = parseFloat(match[1]); const lat = parseFloat(match[2]); const lng = parseFloat(match[3]); if (Number.isFinite(lat) && Number.isFinite(lng) && Number.isFinite(z)) { return { center: [lat, lng], zoom: z }; } } return { center, zoom }; } function writeUrlPosition(map) { if (urlUpdateTimer) clearTimeout(urlUpdateTimer); urlUpdateTimer = setTimeout(() => { const c = map.getCenter(); const hash = "#map=" + map.getZoom() + "/" + c.lat.toFixed(5) + "/" + c.lng.toFixed(5); history.replaceState(null, "", hash); }, 100); } function sync(source) { if (syncing) return; syncing = true; maps.forEach(target => { if (target !== source) { target.setView(source.getCenter(), source.getZoom(), { animate: false }); } }); syncing = false; writeUrlPosition(source); updateCrosshairs(); } function setLayer(map, config, currentLayer) { if (currentLayer) map.removeLayer(currentLayer); 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") { return L.tileLayer.wms(config.url, { layers: config.layers, format: config.format || "image/png", transparent: config.transparent ?? true, version: config.version || "1.3.0", attribution: config.attribution || "", referrerPolicy: "strict-origin-when-cross-origin" }).addTo(map); } } function fillMapCountDropdown(select) { select.innerHTML = ""; mapCounts.forEach(count => { const opt = document.createElement("option"); opt.value = count; opt.textContent = count + (count === 2 || count === 4 ? " mapy" : " map"); select.appendChild(opt); }); } function setMapCount(count) { const container = document.getElementById("container"); container.dataset.count = count; for (let i = 1; i <= maps.length; i++) { const wrapper = document.getElementById("map" + i).parentElement; wrapper.classList.toggle("hidden", i > count); } hideCrosshairs(); setTimeout(() => { maps.forEach((map, index) => { if (index < count) map.invalidateSize(false); }); }, 0); } function isVisibleMap(map) { return !map.getContainer().parentElement.classList.contains("hidden"); } function createCrosshair(map) { const crosshair = document.createElement("div"); crosshair.className = "crosshair"; map.getContainer().parentElement.appendChild(crosshair); return crosshair; } function hideCrosshairs() { activeMouseMap = null; activeMousePoint = null; activeMouseLatLng = null; crosshairs.forEach(crosshair => { crosshair.style.display = "none"; }); } function updateCrosshairs() { if (!activeMouseMap || !activeMousePoint || !isVisibleMap(activeMouseMap)) return; activeMouseLatLng = activeMouseMap.containerPointToLatLng(activeMousePoint); maps.forEach((map, index) => { const crosshair = crosshairs[index]; if (map === activeMouseMap || !isVisibleMap(map)) { crosshair.style.display = "none"; return; } const point = map.latLngToContainerPoint(activeMouseLatLng); const size = map.getSize(); if (point.x < 0 || point.y < 0 || point.x > size.x || point.y > size.y) { crosshair.style.display = "none"; return; } crosshair.style.left = point.x + "px"; crosshair.style.top = point.y + "px"; crosshair.style.display = "block"; }); } function initCrosshair(map) { const container = map.getContainer(); map.on("mousemove", e => { activeMouseMap = map; activeMousePoint = map.mouseEventToContainerPoint(e.originalEvent); activeMouseLatLng = e.latlng; updateCrosshairs(); }); map.on("mouseout", e => { if (!container.contains(e.originalEvent.relatedTarget)) hideCrosshairs(); }); map.on("move", () => updateCrosshairs()); map.on("zoom", () => updateCrosshairs()); } function init() { const urlPosition = readUrlPosition(); const mapCountSelector = document.getElementById("mapCountSelector"); fillMapCountDropdown(mapCountSelector); for (let i = 1; i <= 9; i++) { const select = document.getElementById("select" + i); fillDropdown(select); const map = L.map("map" + i, { center: urlPosition.center, zoom: urlPosition.zoom, zoomControl: false }); map.on("move", () => sync(map)); map.on("zoom", () => sync(map)); L.control.zoom({ position: "topright" }).addTo(map); maps.push(map); layers.push(null); 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]); }); } mapCountSelector.value = "2"; mapCountSelector.addEventListener("change", e => { setMapCount(parseInt(e.target.value, 10)); }); setMapCount(parseInt(mapCountSelector.value, 10)); } async function loadTileConfigs(json_files) { const results = await Promise.all( json_files.map(f => fetch(f).then(r => r.json())) ); // Merge all JSON objects into TILE_SERVERS results.forEach(obj => Object.assign(TILE_SERVERS, obj)); } loadTileConfigs(json_files).then(() => { init(); });