]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/layers/data.js
Fix new eslint warnings
[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         headers: { ...OSM.oauth },
101         signal: dataLoader.signal
102       });
103     }
104
105     const requestBounds = getRequestBounds(bounds);
106     const requests = requestBounds.map(fetchDataForBounds);
107
108     Promise.all(requests)
109       .then(responses =>
110         Promise.all(
111           responses.map(async response => {
112             if (response.ok) {
113               return response.json();
114             }
115
116             const status = response.statusText || response.status;
117             if (response.status !== 400 && response.status !== 509) {
118               throw new Error(status);
119             }
120
121             const text = await response.text();
122             throw new Error(text || status);
123           })
124         )
125       )
126       .then(dataArray => {
127         dataLayer.clearLayers();
128         const allElements = dataArray.flatMap(item => item.elements);
129         const originalFeatures = dataLayer.buildFeatures({ elements: allElements });
130         // clone features when crossing antimeridian to work around Leaflet restrictions
131         const features = requestBounds.length > 1 ?
132           [...originalFeatures, ...cloneFeatures(originalFeatures)] : originalFeatures;
133
134         function addFeatures() {
135           $("#browse_status").empty();
136           dataLayer.addData(features);
137           loadedBounds = bounds;
138         }
139
140         function cancelAddFeatures() {
141           $("#browse_status").empty();
142         }
143
144         if (features.length < maxFeatures * requestBounds.length) {
145           addFeatures();
146         } else {
147           displayFeatureWarning(features.length, addFeatures, cancelAddFeatures);
148         }
149
150         if (map._objectLayer) {
151           map._objectLayer.bringToFront();
152         }
153       })
154       .catch(function (error) {
155         if (error.name === "AbortError") return;
156
157         OSM.displayLoadError(error?.message, () => {
158           $("#browse_status").empty();
159         });
160       })
161       .finally(() => {
162         dataLoader = null;
163         spanLoading.remove();
164       });
165   }
166
167   function cloneFeatures(features) {
168     const offset = map.getCenter().lng < 0 ? -360 : 360;
169
170     const cloneNode = ({ latLng, ...rest }) => ({
171       ...rest,
172       latLng: { ...latLng, lng: latLng.lng + offset }
173     });
174
175     return features.flatMap(feature => {
176       if (feature.type === "node") {
177         return [cloneNode(feature)];
178       }
179
180       if (feature.type === "way") {
181         const clonedNodes = feature.nodes.map(cloneNode);
182         return [{ ...feature, nodes: clonedNodes }];
183       }
184
185       return [];
186     });
187   }
188 };