]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/history.js
Merge remote-tracking branch 'upstream/pull/5885'
[rails.git] / app / assets / javascripts / index / history.js
1 //= require jquery-simulate/jquery.simulate
2
3 OSM.History = function (map) {
4   const page = {};
5
6   $("#sidebar_content")
7     .on("click", ".changeset_more a", loadMoreChangesets)
8     .on("mouseover", "[data-changeset]", function () {
9       highlightChangeset($(this).data("changeset").id);
10     })
11     .on("mouseout", "[data-changeset]", function () {
12       unHighlightChangeset($(this).data("changeset").id);
13     });
14
15   const group = L.featureGroup()
16     .on("mouseover", function (e) {
17       highlightChangeset(e.layer.id);
18     })
19     .on("mouseout", function (e) {
20       unHighlightChangeset(e.layer.id);
21     })
22     .on("click", function (e) {
23       clickChangeset(e.layer.id, e.originalEvent);
24     });
25
26   group.getLayerId = function (layer) {
27     return layer.id;
28   };
29
30   let changesetIntersectionObserver;
31
32   function disableChangesetIntersectionObserver() {
33     if (changesetIntersectionObserver) {
34       changesetIntersectionObserver.disconnect();
35       changesetIntersectionObserver = null;
36     }
37   }
38
39   function enableChangesetIntersectionObserver() {
40     disableChangesetIntersectionObserver();
41     if (!window.IntersectionObserver) return;
42
43     let ignoreIntersectionEvents = true;
44
45     changesetIntersectionObserver = new IntersectionObserver((entries) => {
46       if (ignoreIntersectionEvents) {
47         ignoreIntersectionEvents = false;
48         return;
49       }
50
51       let closestTargetToTop,
52           closestDistanceToTop = Infinity,
53           closestTargetToBottom,
54           closestDistanceToBottom = Infinity;
55
56       for (const entry of entries) {
57         if (entry.isIntersecting) continue;
58
59         const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
60         const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
61         if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
62           closestDistanceToTop = distanceToTop;
63           closestTargetToTop = entry.target;
64         }
65         if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
66           closestDistanceToBottom = distanceToBottom;
67           closestTargetToBottom = entry.target;
68         }
69       }
70
71       if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
72         const id = $(closestTargetToTop).data("changeset")?.id;
73         if (id) {
74           OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
75         }
76       } else if (closestTargetToBottom) {
77         const id = $(closestTargetToBottom).data("changeset")?.id;
78         if (id) {
79           OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
80         }
81       }
82     }, { root: $("#sidebar")[0] });
83
84     $("#sidebar_content .changesets ol").children().each(function () {
85       changesetIntersectionObserver.observe(this);
86     });
87   }
88
89   function highlightChangeset(id) {
90     const layer = group.getLayer(id);
91     if (layer) layer.setStyle({ fillOpacity: 0.3, color: "#FF6600", weight: 3 });
92     $("#changeset_" + id).addClass("selected");
93   }
94
95   function unHighlightChangeset(id) {
96     const layer = group.getLayer(id);
97     if (layer) layer.setStyle({ fillOpacity: 0, color: "#FF9500", weight: 2 });
98     $("#changeset_" + id).removeClass("selected");
99   }
100
101   function clickChangeset(id, e) {
102     $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
103   }
104
105   function displayFirstChangesets(html) {
106     $("#sidebar_content .changesets").html(html);
107
108     if (location.pathname === "/history") {
109       setPaginationMapHashes();
110     }
111   }
112
113   function displayMoreChangesets(div, html) {
114     const sidebar = $("#sidebar")[0];
115     const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
116
117     const oldList = $("#sidebar_content .changesets ol");
118
119     div.replaceWith(html);
120
121     const prevNewList = oldList.prevAll("ol");
122     if (prevNewList.length) {
123       prevNewList.next(".changeset_more").remove();
124       prevNewList.children().prependTo(oldList);
125       prevNewList.remove();
126
127       // restore scroll position only if prepending
128       sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
129     }
130
131     const nextNewList = oldList.nextAll("ol");
132     if (nextNewList.length) {
133       nextNewList.prev(".changeset_more").remove();
134       nextNewList.children().appendTo(oldList);
135       nextNewList.remove();
136     }
137
138     if (location.pathname === "/history") {
139       setPaginationMapHashes();
140     }
141   }
142
143   function setPaginationMapHashes() {
144     $("#sidebar .pagination a").each(function () {
145       $(this).prop("hash", OSM.formatHash({
146         center: map.getCenter(),
147         zoom: map.getZoom()
148       }));
149     });
150   }
151
152   function loadFirstChangesets() {
153     const data = new URLSearchParams();
154
155     disableChangesetIntersectionObserver();
156
157     if (location.pathname === "/history") {
158       setBboxFetchData(data);
159       const feedLink = $("link[type=\"application/atom+xml\"]"),
160             feedHref = feedLink.attr("href").split("?")[0];
161       feedLink.attr("href", feedHref + "?" + data);
162     }
163
164     setListFetchData(data, location);
165
166     fetch(location.pathname + "?" + data)
167       .then(response => response.text())
168       .then(function (html) {
169         displayFirstChangesets(html);
170         enableChangesetIntersectionObserver();
171
172         if (data.has("before")) {
173           const [firstItem] = $("#sidebar_content .changesets ol").children().first();
174           firstItem?.scrollIntoView();
175         } else if (data.has("after")) {
176           const [lastItem] = $("#sidebar_content .changesets ol").children().last();
177           lastItem?.scrollIntoView(false);
178         } else {
179           const [sidebar] = $("#sidebar");
180           sidebar.scrollTop = 0;
181         }
182
183         updateMap();
184       });
185   }
186
187   function loadMoreChangesets(e) {
188     e.preventDefault();
189     e.stopPropagation();
190
191     const div = $(this).parents(".changeset_more");
192
193     div.find(".pagination").addClass("invisible");
194     div.find("[hidden]").prop("hidden", false);
195
196     const data = new URLSearchParams();
197
198     if (location.pathname === "/history") {
199       setBboxFetchData(data);
200     }
201
202     const url = new URL($(this).attr("href"), location);
203     setListFetchData(data, url);
204
205     fetch(url.pathname + "?" + data)
206       .then(response => response.text())
207       .then(function (html) {
208         displayMoreChangesets(div, html);
209         enableChangesetIntersectionObserver();
210
211         updateMap();
212       });
213   }
214
215   function setBboxFetchData(data) {
216     const crs = map.options.crs;
217     const sw = map.getBounds().getSouthWest();
218     const ne = map.getBounds().getNorthEast();
219     const swClamped = crs.unproject(crs.project(sw));
220     const neClamped = crs.unproject(crs.project(ne));
221
222     if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
223       data.set("bbox", map.getBounds().wrap().toBBoxString());
224     }
225   }
226
227   function setListFetchData(data, url) {
228     const params = new URLSearchParams(url.search);
229
230     data.set("list", "1");
231
232     if (params.has("before")) {
233       data.set("before", params.get("before"));
234     }
235     if (params.has("after")) {
236       data.set("after", params.get("after"));
237     }
238   }
239
240   function reloadChangesetsBecauseOfMapMovement() {
241     OSM.router.replace("/history" + window.location.hash);
242     loadFirstChangesets();
243   }
244
245   let changesets = [];
246
247   function updateBounds() {
248     group.clearLayers();
249
250     for (const changeset of changesets) {
251       const bottomLeft = map.project(L.latLng(changeset.bbox.minlat, changeset.bbox.minlon)),
252             topRight = map.project(L.latLng(changeset.bbox.maxlat, changeset.bbox.maxlon)),
253             width = topRight.x - bottomLeft.x,
254             height = bottomLeft.y - topRight.y,
255             minSize = 20; // Min width/height of changeset in pixels
256
257       if (width < minSize) {
258         bottomLeft.x -= ((minSize - width) / 2);
259         topRight.x += ((minSize - width) / 2);
260       }
261
262       if (height < minSize) {
263         bottomLeft.y += ((minSize - height) / 2);
264         topRight.y -= ((minSize - height) / 2);
265       }
266
267       changeset.bounds = L.latLngBounds(map.unproject(bottomLeft),
268                                         map.unproject(topRight));
269     }
270
271     changesets.sort(function (a, b) {
272       return b.bounds.getSize() - a.bounds.getSize();
273     });
274
275     for (const changeset of changesets) {
276       const rect = L.rectangle(changeset.bounds,
277                                { weight: 2, color: "#FF9500", opacity: 1, fillColor: "#FFFFAF", fillOpacity: 0 });
278       rect.id = changeset.id;
279       rect.addTo(group);
280     }
281   }
282
283   function updateMap() {
284     changesets = $("[data-changeset]").map(function (index, element) {
285       return $(element).data("changeset");
286     }).get().filter(function (changeset) {
287       return changeset.bbox;
288     });
289
290     updateBounds();
291
292     if (location.pathname !== "/history") {
293       const bounds = group.getBounds();
294       if (bounds.isValid()) map.fitBounds(bounds);
295     }
296   }
297
298   page.pushstate = page.popstate = function (path) {
299     OSM.loadSidebarContent(path, page.load);
300   };
301
302   page.load = function () {
303     map.addLayer(group);
304
305     if (location.pathname === "/history") {
306       map.on("moveend", reloadChangesetsBecauseOfMapMovement);
307     }
308
309     map.on("zoomend", updateBounds);
310
311     loadFirstChangesets();
312   };
313
314   page.unload = function () {
315     map.removeLayer(group);
316     map.off("moveend", reloadChangesetsBecauseOfMapMovement);
317     map.off("zoomend", updateBounds);
318     disableChangesetIntersectionObserver();
319   };
320
321   return page;
322 };