1 //= require jquery-simulate/jquery.simulate
2 //= require ./history-changesets-layer
4 OSM.History = function (map) {
8 .on("click", ".changeset_more a", loadMoreChangesets)
9 .on("mouseover", "[data-changeset]", function () {
10 toggleChangesetHighlight($(this).data("changeset").id, true);
12 .on("mouseout", "[data-changeset]", function () {
13 toggleChangesetHighlight($(this).data("changeset").id, false);
17 map.on("zoomstart", () => inZoom = true);
18 map.on("zoomend", () => inZoom = false);
20 const changesetsLayer = new OSM.HistoryChangesetsLayer()
21 .on("mouseover", function (e) {
23 toggleChangesetHighlight(e.layer.id, true);
25 .on("mouseout", function (e) {
27 toggleChangesetHighlight(e.layer.id, false);
29 .on("click", function (e) {
30 clickChangeset(e.layer.id, e.originalEvent);
32 .on("requestscrolltochangeset", function (e) {
33 const [item] = $(`#changeset_${e.id}`);
34 item?.scrollIntoView({ block: "nearest", behavior: "smooth" });
37 let changesetIntersectionObserver;
39 function disableChangesetIntersectionObserver() {
40 if (changesetIntersectionObserver) {
41 changesetIntersectionObserver.disconnect();
42 changesetIntersectionObserver = null;
46 function enableChangesetIntersectionObserver() {
47 disableChangesetIntersectionObserver();
48 if (!window.IntersectionObserver) return;
50 let keepInitialLocation = true;
51 let itemsInViewport = $();
53 changesetIntersectionObserver = new IntersectionObserver((entries) => {
54 let closestTargetToTop,
55 closestDistanceToTop = Infinity,
56 closestTargetToBottom,
57 closestDistanceToBottom = Infinity;
59 for (const entry of entries) {
60 const id = $(entry.target).data("changeset")?.id;
62 if (entry.isIntersecting) {
63 itemsInViewport = itemsInViewport.add(entry.target);
64 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 0);
67 itemsInViewport = itemsInViewport.not(entry.target);
70 const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
71 const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
73 if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
74 closestDistanceToTop = distanceToTop;
75 closestTargetToTop = entry.target;
77 if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
78 closestDistanceToBottom = distanceToBottom;
79 closestTargetToBottom = entry.target;
83 itemsInViewport.first().prevAll().each(function () {
84 const id = $(this).data("changeset")?.id;
85 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 1);
87 itemsInViewport.last().nextAll().each(function () {
88 const id = $(this).data("changeset")?.id;
89 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, -1);
92 changesetsLayer.updateChangesetsOrder();
94 if (keepInitialLocation) {
95 keepInitialLocation = false;
99 if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
100 const id = $(closestTargetToTop).data("changeset")?.id;
102 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
104 } else if (closestTargetToBottom) {
105 const id = $(closestTargetToBottom).data("changeset")?.id;
107 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
110 }, { root: $("#sidebar")[0] });
112 $("#sidebar_content .changesets ol").children().each(function () {
113 changesetIntersectionObserver.observe(this);
117 function toggleChangesetHighlight(id, state) {
118 changesetsLayer.toggleChangesetHighlight(id, state);
119 $("#sidebar_content .changesets ol li").removeClass("selected");
121 $("#changeset_" + id).addClass("selected");
125 function clickChangeset(id, e) {
126 $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
129 function displayFirstChangesets(html) {
130 $("#sidebar_content .changesets").html(html);
132 $("#sidebar_content .changesets ol")
133 .before($("<div class='changeset-color-hint-bar opacity-75 sticky-top changeset-above-sidebar-viewport'>"))
134 .after($("<div class='changeset-color-hint-bar opacity-75 sticky-bottom changeset-below-sidebar-viewport'>"));
136 if (location.pathname === "/history") {
137 setPaginationMapHashes();
141 function displayMoreChangesets(div, html) {
142 const sidebar = $("#sidebar")[0];
143 const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
145 const oldList = $("#sidebar_content .changesets ol");
147 div.replaceWith(html);
149 const prevNewList = oldList.prevAll("ol");
150 if (prevNewList.length) {
151 prevNewList.next(".changeset_more").remove();
152 prevNewList.children().prependTo(oldList);
153 prevNewList.remove();
155 // restore scroll position only if prepending
156 sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
159 const nextNewList = oldList.nextAll("ol");
160 if (nextNewList.length) {
161 nextNewList.prev(".changeset_more").remove();
162 nextNewList.children().appendTo(oldList);
163 nextNewList.remove();
166 if (location.pathname === "/history") {
167 setPaginationMapHashes();
171 function setPaginationMapHashes() {
172 $("#sidebar .pagination a").each(function () {
173 $(this).prop("hash", OSM.formatHash(map));
177 function loadFirstChangesets() {
178 const data = new URLSearchParams();
180 disableChangesetIntersectionObserver();
182 if (location.pathname === "/history") {
183 setBboxFetchData(data);
184 const feedLink = $("link[type=\"application/atom+xml\"]"),
185 feedHref = feedLink.attr("href").split("?")[0];
186 feedLink.attr("href", feedHref + "?" + data);
189 setListFetchData(data, location);
191 fetch(location.pathname + "?" + data)
192 .then(response => response.text())
193 .then(function (html) {
194 displayFirstChangesets(html);
195 enableChangesetIntersectionObserver();
197 if (data.has("before")) {
198 const [firstItem] = $("#sidebar_content .changesets ol").children().first();
199 firstItem?.scrollIntoView();
200 } else if (data.has("after")) {
201 const [lastItem] = $("#sidebar_content .changesets ol").children().last();
202 lastItem?.scrollIntoView(false);
204 const [sidebar] = $("#sidebar");
205 sidebar.scrollTop = 0;
212 function loadMoreChangesets(e) {
216 const div = $(this).parents(".changeset_more");
218 div.find(".pagination").addClass("invisible");
219 div.find("[hidden]").prop("hidden", false);
221 const data = new URLSearchParams();
223 if (location.pathname === "/history") {
224 setBboxFetchData(data);
227 const url = new URL($(this).attr("href"), location);
228 setListFetchData(data, url);
230 fetch(url.pathname + "?" + data)
231 .then(response => response.text())
232 .then(function (html) {
233 displayMoreChangesets(div, html);
234 enableChangesetIntersectionObserver();
240 function setBboxFetchData(data) {
241 const crs = map.options.crs;
242 const sw = map.getBounds().getSouthWest();
243 const ne = map.getBounds().getNorthEast();
244 const swClamped = crs.unproject(crs.project(sw));
245 const neClamped = crs.unproject(crs.project(ne));
247 if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
248 data.set("bbox", map.getBounds().toBBoxString());
252 function setListFetchData(data, url) {
253 const params = new URLSearchParams(url.search);
255 data.set("list", "1");
257 if (params.has("before")) {
258 data.set("before", params.get("before"));
260 if (params.has("after")) {
261 data.set("after", params.get("after"));
265 function moveEndListener() {
266 if (location.pathname === "/history") {
267 OSM.router.replace("/history" + window.location.hash);
268 loadFirstChangesets();
270 $("#sidebar_content .changesets ol li").removeClass("selected");
271 changesetsLayer.updateChangesetsGeometry(map);
275 function zoomEndListener() {
276 $("#sidebar_content .changesets ol li").removeClass("selected");
277 changesetsLayer.updateChangesetsGeometry(map);
280 function updateMap() {
281 const changesets = $("[data-changeset]").map(function (index, element) {
282 return $(element).data("changeset");
283 }).get().filter(function (changeset) {
284 return changeset.bbox;
287 changesetsLayer.updateChangesets(map, changesets);
289 if (location.pathname !== "/history") {
290 const bounds = changesetsLayer.getBounds();
291 if (bounds.isValid()) map.fitBounds(bounds);
295 page.pushstate = page.popstate = function (path) {
296 OSM.loadSidebarContent(path, page.load);
299 page.load = function () {
300 map.addLayer(changesetsLayer);
301 map.on("moveend", moveEndListener);
302 map.on("zoomend", zoomEndListener);
303 loadFirstChangesets();
306 page.unload = function () {
307 map.removeLayer(changesetsLayer);
308 map.off("moveend", moveEndListener);
309 map.off("zoomend", zoomEndListener);
310 disableChangesetIntersectionObserver();