From 3adc3569bceaf37858cd273247f94666ba49866f Mon Sep 17 00:00:00 2001 From: mmd-osm Date: Sun, 10 Aug 2025 22:53:17 +0200 Subject: [PATCH] Antimeridian map data display --- app/assets/javascripts/index/layers/data.js | 103 ++++++++++++++++---- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/index/layers/data.js b/app/assets/javascripts/index/layers/data.js index 06fa26ee9..0ba0cf60e 100644 --- a/app/assets/javascripts/index/layers/data.js +++ b/app/assets/javascripts/index/layers/data.js @@ -64,14 +64,12 @@ OSM.initializeDataLayer = function (map) { } function getData() { - const bounds = map.getBounds(); - const url = "/api/" + OSM.API_VERSION + "/map.json?bbox=" + bounds.toBBoxString(); - /* * Modern browsers are quite happy showing far more than 100 features in * the data browser, so increase the limit to 4000. */ const maxFeatures = 4000; + const bounds = map.getBounds(); if (dataLoader) dataLoader.abort(); @@ -85,19 +83,65 @@ OSM.initializeDataLayer = function (map) { .appendTo($("#label-layers-data")); dataLoader = new AbortController(); - fetch(url, { signal: dataLoader.signal }) - .then(response => { - if (response.ok) return response.json(); - const status = response.statusText || response.status; - if (response.status !== 400 && response.status !== 509) throw new Error(status); - return response.text().then(text => { - throw new Error(text || status); - }); - }) - .then(function (data) { - dataLayer.clearLayers(); - const features = dataLayer.buildFeatures(data); + function getWrappedBounds(bounds) { + const sw = bounds.getSouthWest().wrap(); + const ne = bounds.getNorthEast().wrap(); + return { + minLat: sw.lat, + minLng: sw.lng, + maxLat: ne.lat, + maxLng: ne.lng + }; + } + + function getRequestBounds(bounds) { + const wrapped = getWrappedBounds(bounds); + if (wrapped.minLng > wrapped.maxLng) { + // BBox is crossing antimeridian: split into two bboxes in order to stay + // within OSM API's map endpoint permitted range for longitude [-180..180]. + return [ + L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, 180]), + L.latLngBounds([wrapped.minLat, -180], [wrapped.maxLat, wrapped.maxLng]) + ]; + } + return [L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, wrapped.maxLng])]; + } + + function fetchDataForBounds(bounds) { + return fetch(`/api/${OSM.API_VERSION}/map.json?bbox=${bounds.toBBoxString()}`, { + signal: dataLoader.signal + }); + } + + const requestBounds = getRequestBounds(bounds); + const requests = requestBounds.map(fetchDataForBounds); + + Promise.all(requests) + .then(responses => + Promise.all( + responses.map(async response => { + if (response.ok) { + return response.json(); + } + + const status = response.statusText || response.status; + if (response.status !== 400 && response.status !== 509) { + throw new Error(status); + } + + const text = await response.text(); + throw new Error(text || status); + }) + ) + ) + .then(dataArray => { + dataLayer.clearLayers(); + const allElements = dataArray.flatMap(item => item.elements); + const originalFeatures = dataLayer.buildFeatures({ elements: allElements }); + // clone features when crossing antimeridian to work around Leaflet restrictions + const features = requestBounds.length > 1 ? + [...originalFeatures, ...cloneFeatures(originalFeatures)] : originalFeatures; function addFeatures() { $("#browse_status").empty(); @@ -109,7 +153,7 @@ OSM.initializeDataLayer = function (map) { $("#browse_status").empty(); } - if (features.length < maxFeatures) { + if (features.length < maxFeatures * requestBounds.length) { addFeatures(); } else { displayFeatureWarning(features.length, addFeatures, cancelAddFeatures); @@ -118,8 +162,6 @@ OSM.initializeDataLayer = function (map) { if (map._objectLayer) { map._objectLayer.bringToFront(); } - - dataLoader = null; }) .catch(function (error) { if (error.name === "AbortError") return; @@ -127,11 +169,32 @@ OSM.initializeDataLayer = function (map) { displayLoadError(error?.message, () => { $("#browse_status").empty(); }); - - dataLoader = null; }) .finally(() => { + dataLoader = null; spanLoading.remove(); }); } + + function cloneFeatures(features) { + const offset = map.getCenter().lng < 0 ? -360 : 360; + + const cloneNode = ({ latLng, ...rest }) => ({ + ...rest, + latLng: { ...latLng, lng: latLng.lng + offset } + }); + + return features.flatMap(feature => { + if (feature.type === "node") { + return [cloneNode(feature)]; + } + + if (feature.type === "way") { + const clonedNodes = feature.nodes.map(cloneNode); + return [{ ...feature, nodes: clonedNodes }]; + } + + return []; + }); + } }; -- 2.39.5