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