1 OSM.initializeDataLayer = function (map) {
2 let dataLoader, loadedBounds;
3 const dataLayer = map.dataLayer;
5 dataLayer.isWayArea = function () {
9 dataLayer.on("click", function (e) {
10 const feature = e.layer.feature;
11 OSM.router.click(e.originalEvent, `/${feature.type}/${feature.id}`);
14 dataLayer.on("add", function () {
15 map.fire("overlayadd", { layer: this });
16 map.on("moveend", updateData);
20 dataLayer.on("remove", function () {
21 if (dataLoader) dataLoader.abort();
23 map.off("moveend", updateData);
24 $("#browse_status").empty();
25 map.fire("overlayremove", { layer: this });
28 function updateData() {
29 const bounds = map.getBounds();
30 if (!loadedBounds || !loadedBounds.contains(bounds)) {
35 function displayFeatureWarning(num_features, add, cancel) {
36 $("#browse_status").html(
37 $("<div class='p-3'>").append(
38 $("<div class='d-flex'>").append(
39 $("<h2 class='flex-grow-1 text-break'>")
40 .text(OSM.i18n.t("browse.start_rjs.load_data")),
42 $("<button type='button' class='btn-close'>")
43 .attr("aria-label", OSM.i18n.t("javascripts.close"))
45 $("<p class='alert alert-warning'>")
46 .text(OSM.i18n.t("browse.start_rjs.feature_warning", { num_features })),
47 $("<input type='submit' class='btn btn-primary d-block mx-auto'>")
48 .val(OSM.i18n.t("browse.start_rjs.load_data"))
52 function displayLoadError(message, close) {
53 $("#browse_status").html(
54 $("<div class='p-3'>").append(
55 $("<div class='d-flex'>").append(
56 $("<h2 class='flex-grow-1 text-break'>")
57 .text(OSM.i18n.t("browse.start_rjs.load_data")),
59 $("<button type='button' class='btn-close'>")
60 .attr("aria-label", OSM.i18n.t("javascripts.close"))
62 $("<p class='alert alert-warning'>")
63 .text(OSM.i18n.t("browse.start_rjs.feature_error", { message: message }))));
68 * Modern browsers are quite happy showing far more than 100 features in
69 * the data browser, so increase the limit to 4000.
71 const maxFeatures = 4000;
72 const bounds = map.getBounds();
74 if (dataLoader) dataLoader.abort();
76 $("#layers-data-loading").remove();
78 const spanLoading = $("<span>")
79 .attr("id", "layers-data-loading")
80 .attr("class", "spinner-border spinner-border-sm ms-1")
81 .attr("role", "status")
82 .html("<span class='visually-hidden'>" + OSM.i18n.t("browse.start_rjs.loading") + "</span>")
83 .appendTo($("#label-layers-data"));
85 dataLoader = new AbortController();
87 function getWrappedBounds(bounds) {
88 const sw = bounds.getSouthWest().wrap();
89 const ne = bounds.getNorthEast().wrap();
98 function getRequestBounds(bounds) {
99 const wrapped = getWrappedBounds(bounds);
100 if (wrapped.minLng > wrapped.maxLng) {
101 // BBox is crossing antimeridian: split into two bboxes in order to stay
102 // within OSM API's map endpoint permitted range for longitude [-180..180].
104 L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, 180]),
105 L.latLngBounds([wrapped.minLat, -180], [wrapped.maxLat, wrapped.maxLng])
108 return [L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, wrapped.maxLng])];
111 function fetchDataForBounds(bounds) {
112 return fetch(`/api/${OSM.API_VERSION}/map.json?bbox=${bounds.toBBoxString()}`, {
113 signal: dataLoader.signal
117 const requestBounds = getRequestBounds(bounds);
118 const requests = requestBounds.map(fetchDataForBounds);
120 Promise.all(requests)
123 responses.map(async response => {
125 return response.json();
128 const status = response.statusText || response.status;
129 if (response.status !== 400 && response.status !== 509) {
130 throw new Error(status);
133 const text = await response.text();
134 throw new Error(text || status);
139 dataLayer.clearLayers();
140 const allElements = dataArray.flatMap(item => item.elements);
141 const originalFeatures = dataLayer.buildFeatures({ elements: allElements });
142 // clone features when crossing antimeridian to work around Leaflet restrictions
143 const features = requestBounds.length > 1 ?
144 [...originalFeatures, ...cloneFeatures(originalFeatures)] : originalFeatures;
146 function addFeatures() {
147 $("#browse_status").empty();
148 dataLayer.addData(features);
149 loadedBounds = bounds;
152 function cancelAddFeatures() {
153 $("#browse_status").empty();
156 if (features.length < maxFeatures * requestBounds.length) {
159 displayFeatureWarning(features.length, addFeatures, cancelAddFeatures);
162 if (map._objectLayer) {
163 map._objectLayer.bringToFront();
166 .catch(function (error) {
167 if (error.name === "AbortError") return;
169 displayLoadError(error?.message, () => {
170 $("#browse_status").empty();
175 spanLoading.remove();
179 function cloneFeatures(features) {
180 const offset = map.getCenter().lng < 0 ? -360 : 360;
182 const cloneNode = ({ latLng, ...rest }) => ({
184 latLng: { ...latLng, lng: latLng.lng + offset }
187 return features.flatMap(feature => {
188 if (feature.type === "node") {
189 return [cloneNode(feature)];
192 if (feature.type === "way") {
193 const clonedNodes = feature.nodes.map(cloneNode);
194 return [{ ...feature, nodes: clonedNodes }];