]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/layers/data.js
Merge remote-tracking branch 'upstream/pull/6360'
[rails.git] / app / assets / javascripts / index / layers / data.js
1 //= require download_util
2 OSM.initializeDataLayer = function (map) {
3   let dataLoader, loadedBounds;
4   const dataLayer = map.dataLayer;
5
6   dataLayer.isWayArea = function () {
7     return false;
8   };
9
10   dataLayer.on("click", function (e) {
11     const feature = e.layer.feature;
12     OSM.router.click(e.originalEvent, `/${feature.type}/${feature.id}`);
13   });
14
15   dataLayer.on("add", function () {
16     map.fire("overlayadd", { layer: this });
17     map.on("moveend", updateData);
18     updateData();
19   });
20
21   dataLayer.on("remove", function () {
22     if (dataLoader) dataLoader.abort();
23     dataLoader = null;
24     map.off("moveend", updateData);
25     $("#browse_status").empty();
26     map.fire("overlayremove", { layer: this });
27   });
28
29   function updateData() {
30     const bounds = map.getBounds();
31     if (!loadedBounds || !loadedBounds.contains(bounds)) {
32       getData();
33     }
34   }
35
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")),
42           $("<div>").append(
43             $("<button type='button' class='btn-close'>")
44               .attr("aria-label", OSM.i18n.t("javascripts.close"))
45               .click(cancel))),
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"))
50           .click(add)));
51   }
52
53   function getData() {
54     /*
55      * Modern browsers are quite happy showing far more than 100 features in
56      * the data browser, so increase the limit to 4000.
57      */
58     const maxFeatures = 4000;
59     const bounds = map.getBounds();
60
61     if (dataLoader) dataLoader.abort();
62
63     $("#layers-data-loading").remove();
64
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"));
71
72     dataLoader = new AbortController();
73
74     function getWrappedBounds(bounds) {
75       const sw = bounds.getSouthWest().wrap();
76       const ne = bounds.getNorthEast().wrap();
77       return {
78         minLat: sw.lat,
79         minLng: sw.lng,
80         maxLat: ne.lat,
81         maxLng: ne.lng
82       };
83     }
84
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].
90         return [
91           L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, 180]),
92           L.latLngBounds([wrapped.minLat, -180], [wrapped.maxLat, wrapped.maxLng])
93         ];
94       }
95       return [L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, wrapped.maxLng])];
96     }
97
98     function fetchDataForBounds(bounds) {
99       return fetch(`/api/${OSM.API_VERSION}/map.json?bbox=${bounds.toBBoxString()}`, {
100         signal: dataLoader.signal
101       });
102     }
103
104     const requestBounds = getRequestBounds(bounds);
105     const requests = requestBounds.map(fetchDataForBounds);
106
107     Promise.all(requests)
108       .then(responses =>
109         Promise.all(
110           responses.map(async response => {
111             if (response.ok) {
112               return response.json();
113             }
114
115             const status = response.statusText || response.status;
116             if (response.status !== 400 && response.status !== 509) {
117               throw new Error(status);
118             }
119
120             const text = await response.text();
121             throw new Error(text || status);
122           })
123         )
124       )
125       .then(dataArray => {
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;
132
133         function addFeatures() {
134           $("#browse_status").empty();
135           dataLayer.addData(features);
136           loadedBounds = bounds;
137         }
138
139         function cancelAddFeatures() {
140           $("#browse_status").empty();
141         }
142
143         if (features.length < maxFeatures * requestBounds.length) {
144           addFeatures();
145         } else {
146           displayFeatureWarning(features.length, addFeatures, cancelAddFeatures);
147         }
148
149         if (map._objectLayer) {
150           map._objectLayer.bringToFront();
151         }
152       })
153       .catch(function (error) {
154         if (error.name === "AbortError") return;
155
156         OSM.displayLoadError(error?.message, () => {
157           $("#browse_status").empty();
158         });
159       })
160       .finally(() => {
161         dataLoader = null;
162         spanLoading.remove();
163       });
164   }
165
166   function cloneFeatures(features) {
167     const offset = map.getCenter().lng < 0 ? -360 : 360;
168
169     const cloneNode = ({ latLng, ...rest }) => ({
170       ...rest,
171       latLng: { ...latLng, lng: latLng.lng + offset }
172     });
173
174     return features.flatMap(feature => {
175       if (feature.type === "node") {
176         return [cloneNode(feature)];
177       }
178
179       if (feature.type === "way") {
180         const clonedNodes = feature.nodes.map(cloneNode);
181         return [{ ...feature, nodes: clonedNodes }];
182       }
183
184       return [];
185     });
186   }
187 };