]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index_modules/query.js
Merge remote-tracking branch 'upstream/pull/7190'
[rails.git] / app / assets / javascripts / index_modules / query.js
1 //= require feature_label
2
3 export default function (map) {
4   const uninterestingTags = ["source", "source_ref", "source:ref", "history", "attribution", "created_by", "tiger:county", "tiger:tlid", "tiger:upload_uuid", "KSJ2:curve_id", "KSJ2:lat", "KSJ2:lon", "KSJ2:coordinate", "KSJ2:filename", "note:ja"];
5   let marker;
6
7   const featureStyle = {
8     color: "#FF6200",
9     weight: 4,
10     opacity: 1,
11     fillOpacity: 0.5,
12     interactive: false
13   };
14
15   function showResultGeometry() {
16     const geometry = $(this).data("geometry");
17     if (geometry) map.addLayer(geometry);
18     $(this).addClass("selected");
19   }
20
21   function hideResultGeometry() {
22     const geometry = $(this).data("geometry");
23     if (geometry) map.removeLayer(geometry);
24     $(this).removeClass("selected");
25   }
26
27   $("#sidebar_content")
28     .on("mouseover", ".query-results a", showResultGeometry)
29     .on("mouseout", ".query-results a", hideResultGeometry);
30
31   function interestingFeature(feature) {
32     if (feature.tags) {
33       for (const key in feature.tags) {
34         if (uninterestingTags.indexOf(key) < 0) {
35           return true;
36         }
37       }
38     }
39
40     return false;
41   }
42
43   function featureGeometry(feature) {
44     switch (feature.type) {
45       case "node":
46         if (!feature.lat || !feature.lon) return;
47         return L.circleMarker([feature.lat, feature.lon], featureStyle);
48       case "way":
49         if (!feature.geometry?.length) return;
50         return L.polyline(feature.geometry.filter(p => p).map(p => [p.lat, p.lon]), featureStyle);
51       case "relation":
52         if (!feature.members?.length) return;
53         return L.featureGroup(feature.members.map(featureGeometry).filter(g => g));
54     }
55   }
56
57   function runQuery(query, $section, merge, compare) {
58     const $ul = $section.find("ul");
59
60     $ul.empty();
61     $section.show();
62
63     if ($section.data("ajax")) {
64       $section.data("ajax").abort();
65     }
66
67     $section.data("ajax", new AbortController());
68     fetch(OSM.OVERPASS_URL, {
69       method: "POST",
70       body: new URLSearchParams({
71         data: "[timeout:10][out:json];" + query
72       }),
73       credentials: OSM.OVERPASS_CREDENTIALS ? "include" : "same-origin",
74       signal: $section.data("ajax").signal
75     })
76       .then(response => {
77         if (response.ok) {
78           return response.json();
79         }
80         throw new Error(response.statusText || response.status);
81       })
82       .then(function (results) {
83         let elements = results.elements;
84
85         $section.find(".loader").hide();
86
87         // Make Overpass-specific bounds to Leaflet compatible
88         for (const element of elements) {
89           if (!element.bounds) continue;
90           if (element.bounds.maxlon >= element.bounds.minlon) continue;
91           element.bounds.maxlon += 360;
92         }
93
94         if (merge) {
95           elements = Object.values(elements.reduce(function (hash, element) {
96             const key = element.type + element.id;
97             if ("geometry" in element) delete element.bounds;
98             hash[key] = { ...hash[key], ...element };
99             return hash;
100           }, {}));
101         }
102
103         if (compare) {
104           elements = elements.sort(compare);
105         }
106
107         for (const element of elements) {
108           if (!interestingFeature(element)) continue;
109
110           const $li = $("<li>")
111             .addClass("list-group-item list-group-item-action")
112             .text(OSM.featurePrefix(element) + " ")
113             .appendTo($ul);
114
115           $("<a>")
116             .addClass("stretched-link")
117             .attr("href", "/" + element.type + "/" + element.id)
118             .data("geometry", featureGeometry(element))
119             .text(OSM.featureName(element))
120             .appendTo($li);
121         }
122
123         if (results.remark) renderError($ul, results.remark);
124
125         if ($ul.find("li").length === 0) {
126           $("<li>")
127             .addClass("list-group-item")
128             .text(OSM.i18n.t("javascripts.query.nothing_found"))
129             .appendTo($ul);
130         }
131       })
132       .catch(function (error) {
133         if (error.name === "AbortError") return;
134
135         $section.find(".loader").hide();
136
137         renderError($ul, error.message);
138       });
139   }
140
141   function renderError($ul, errorMessage) {
142     $("<li>")
143       .addClass("list-group-item")
144       .text(OSM.i18n.t("javascripts.query.error", { server: OSM.OVERPASS_URL, error: errorMessage }))
145       .appendTo($ul);
146   }
147
148   function size({ maxlon, minlon, maxlat, minlat }) {
149     return (maxlon - minlon) * (maxlat - minlat);
150   }
151
152   /*
153    * To find nearby objects we ask overpass for the union of the
154    * following sets:
155    *
156    *   node(around:<radius>,<lat>,<lng>)
157    *   way(around:<radius>,<lat>,<lng>)
158    *   relation(around:<radius>,<lat>,<lng>)
159    *
160    * to find enclosing objects we first find all the enclosing areas:
161    *
162    *   is_in(<lat>,<lng>)->.a
163    *
164    * and then return the union of the following sets:
165    *
166    *   relation(pivot.a)
167    *   way(pivot.a)
168    *
169    * In both cases we then ask to retrieve tags and the geometry
170    * for each object.
171    */
172   function queryOverpass(latlng) {
173     const bounds = map.getBounds(),
174           zoom = map.getZoom(),
175           sw = OSM.cropLocation(bounds.getSouthWest(), zoom),
176           ne = OSM.cropLocation(bounds.getNorthEast(), zoom),
177           bbox = `${sw.lat},${sw.lng},${ne.lat},${ne.lng}`,
178           geom = `geom(${bbox})`,
179           radius = 10 * Math.pow(1.5, 19 - zoom),
180           here = `(around:${radius},${latlng})`,
181           enclosed = "(pivot.a);out tags bb",
182           nearby = `(node${here};way${here};);out tags ${geom};relation${here};out ${geom};`,
183           isin = `is_in(${latlng})->.a;way${enclosed};out ids ${geom};relation${enclosed};`;
184
185     $("#sidebar_content .query-intro")
186       .hide();
187
188     if (marker) map.removeLayer(marker);
189     marker = L.circle(L.latLng(latlng).wrap(), {
190       radius: radius,
191       className: "query-marker",
192       ...featureStyle
193     }).addTo(map);
194
195     runQuery(nearby, $("#query-nearby"), false);
196     runQuery(isin, $("#query-isin"), true, (feature1, feature2) => size(feature1.bounds) - size(feature2.bounds));
197   }
198
199   const page = {};
200
201   page.pushstate = page.popstate = function (path) {
202     OSM.loadSidebarContent(path, function () {
203       page.load(path, true);
204     });
205   };
206
207   page.load = function (path, noCentre) {
208     const params = new URLSearchParams(path.substring(path.indexOf("?"))),
209           latlng = L.latLng(params.get("lat"), params.get("lon"));
210
211     if (!location.hash && !noCentre && !map.getBounds().contains(latlng)) {
212       OSM.router.withoutMoveListener(function () {
213         map.setView(latlng, 15);
214       });
215     }
216
217     queryOverpass([params.get("lat"), params.get("lon")]);
218   };
219
220   page.unload = function (sameController) {
221     if (!sameController) {
222       $("#sidebar_content .query-results a.selected").each(hideResultGeometry);
223     }
224   };
225
226   return page;
227 }