1 //= require download_util
2 OSM.initializeDataLayer = function (map) {
3 let dataLoader, loadedBounds;
4 const dataLayer = map.dataLayer;
6 dataLayer.isWayArea = function () {
10 dataLayer.on("click", function (e) {
11 const feature = e.layer.feature;
12 OSM.router.click(e.originalEvent, `/${feature.type}/${feature.id}`);
15 dataLayer.on("add", function () {
16 map.fire("overlayadd", { layer: this });
17 map.on("moveend", updateData);
21 dataLayer.on("remove", function () {
22 if (dataLoader) dataLoader.abort();
24 map.off("moveend", updateData);
25 $("#browse_status").empty();
26 map.fire("overlayremove", { layer: this });
29 function updateData() {
30 const bounds = map.getBounds();
31 if (!loadedBounds || !loadedBounds.contains(bounds)) {
36 function displayFeatureWarning(num_features, add, cancel) {
37 $("#browse_status").html(
38 $("<div class='p-3'>").append(
39 $("<div class='d-flex'>").append(
40 $("<h2 class='flex-grow-1 text-break'>")
41 .text(OSM.i18n.t("browse.start_rjs.load_data")),
43 $("<button type='button' class='btn-close'>")
44 .attr("aria-label", OSM.i18n.t("javascripts.close"))
46 $("<p class='alert alert-warning'>")
47 .text(OSM.i18n.t("browse.start_rjs.feature_warning", { num_features })),
48 $("<input type='submit' class='btn btn-primary d-block mx-auto'>")
49 .val(OSM.i18n.t("browse.start_rjs.load_data"))
55 * Modern browsers are quite happy showing far more than 100 features in
56 * the data browser, so increase the limit to 4000.
58 const maxFeatures = 4000;
59 const bounds = map.getBounds();
61 if (dataLoader) dataLoader.abort();
63 $("#layers-data-loading").remove();
65 const spanLoading = $("<span>")
66 .attr("id", "layers-data-loading")
67 .attr("class", "spinner-border spinner-border-sm ms-1")
68 .attr("role", "status")
69 .html("<span class='visually-hidden'>" + OSM.i18n.t("browse.start_rjs.loading") + "</span>")
70 .appendTo($("#label-layers-data"));
72 dataLoader = new AbortController();
74 function getWrappedBounds(bounds) {
75 const sw = bounds.getSouthWest().wrap();
76 const ne = bounds.getNorthEast().wrap();
85 function getRequestBounds(bounds) {
86 const wrapped = getWrappedBounds(bounds);
87 if (wrapped.minLng > wrapped.maxLng) {
88 // BBox is crossing antimeridian: split into two bboxes in order to stay
89 // within OSM API's map endpoint permitted range for longitude [-180..180].
91 L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, 180]),
92 L.latLngBounds([wrapped.minLat, -180], [wrapped.maxLat, wrapped.maxLng])
95 return [L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, wrapped.maxLng])];
98 function fetchDataForBounds(bounds) {
99 return fetch(`/api/${OSM.API_VERSION}/map.json?bbox=${bounds.toBBoxString()}`, {
100 signal: dataLoader.signal
104 const requestBounds = getRequestBounds(bounds);
105 const requests = requestBounds.map(fetchDataForBounds);
107 Promise.all(requests)
110 responses.map(async response => {
112 return response.json();
115 const status = response.statusText || response.status;
116 if (response.status !== 400 && response.status !== 509) {
117 throw new Error(status);
120 const text = await response.text();
121 throw new Error(text || status);
126 dataLayer.clearLayers();
127 const allElements = dataArray.flatMap(item => item.elements);
128 const originalFeatures = dataLayer.buildFeatures({ elements: allElements });
129 // clone features when crossing antimeridian to work around Leaflet restrictions
130 const features = requestBounds.length > 1 ?
131 [...originalFeatures, ...cloneFeatures(originalFeatures)] : originalFeatures;
133 function addFeatures() {
134 $("#browse_status").empty();
135 dataLayer.addData(features);
136 loadedBounds = bounds;
139 function cancelAddFeatures() {
140 $("#browse_status").empty();
143 if (features.length < maxFeatures * requestBounds.length) {
146 displayFeatureWarning(features.length, addFeatures, cancelAddFeatures);
149 if (map._objectLayer) {
150 map._objectLayer.bringToFront();
153 .catch(function (error) {
154 if (error.name === "AbortError") return;
156 OSM.displayLoadError(error?.message, () => {
157 $("#browse_status").empty();
162 spanLoading.remove();
166 function cloneFeatures(features) {
167 const offset = map.getCenter().lng < 0 ? -360 : 360;
169 const cloneNode = ({ latLng, ...rest }) => ({
171 latLng: { ...latLng, lng: latLng.lng + offset }
174 return features.flatMap(feature => {
175 if (feature.type === "node") {
176 return [cloneNode(feature)];
179 if (feature.type === "way") {
180 const clonedNodes = feature.nodes.map(cloneNode);
181 return [{ ...feature, nodes: clonedNodes }];