1 //= require ./history-changesets-layer
3 OSM.History = function (map) {
7 .on("click", ".changeset_more a", loadMoreChangesets)
8 .on("mouseover", "[data-changeset]", function () {
9 toggleChangesetHighlight($(this).data("changeset").id, true);
11 .on("mouseout", "[data-changeset]", function () {
12 toggleChangesetHighlight($(this).data("changeset").id, false);
16 map.on("zoomstart", () => inZoom = true);
17 map.on("zoomend", () => inZoom = false);
19 const changesetsLayer = new OSM.HistoryChangesetsLayer()
20 .on("mouseover", function (e) {
22 toggleChangesetHighlight(e.layer.id, true);
24 .on("mouseout", function (e) {
26 toggleChangesetHighlight(e.layer.id, false);
28 .on("requestscrolltochangeset", function (e) {
29 const [item] = $(`#changeset_${e.id}`);
30 item?.scrollIntoView({ block: "nearest", behavior: "smooth" });
33 let changesetIntersectionObserver;
35 function disableChangesetIntersectionObserver() {
36 if (changesetIntersectionObserver) {
37 changesetIntersectionObserver.disconnect();
38 changesetIntersectionObserver = null;
42 function enableChangesetIntersectionObserver() {
43 disableChangesetIntersectionObserver();
44 if (!window.IntersectionObserver) return;
46 let keepInitialLocation = true;
47 let itemsInViewport = $();
49 changesetIntersectionObserver = new IntersectionObserver((entries) => {
50 let closestTargetToTop,
51 closestDistanceToTop = Infinity,
52 closestTargetToBottom,
53 closestDistanceToBottom = Infinity;
55 for (const entry of entries) {
56 const id = $(entry.target).data("changeset")?.id;
58 if (entry.isIntersecting) {
59 itemsInViewport = itemsInViewport.add(entry.target);
60 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 0);
63 itemsInViewport = itemsInViewport.not(entry.target);
66 const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
67 const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
69 if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
70 closestDistanceToTop = distanceToTop;
71 closestTargetToTop = entry.target;
73 if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
74 closestDistanceToBottom = distanceToBottom;
75 closestTargetToBottom = entry.target;
79 itemsInViewport.first().prevAll().each(function () {
80 const id = $(this).data("changeset")?.id;
81 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 1);
83 itemsInViewport.last().nextAll().each(function () {
84 const id = $(this).data("changeset")?.id;
85 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, -1);
88 changesetsLayer.updateChangesetsOrder();
90 if (keepInitialLocation) {
91 keepInitialLocation = false;
95 if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
96 const id = $(closestTargetToTop).data("changeset")?.id;
98 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
100 } else if (closestTargetToBottom) {
101 const id = $(closestTargetToBottom).data("changeset")?.id;
103 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
106 }, { root: $("#sidebar")[0] });
108 $("#sidebar_content .changesets ol").children().each(function () {
109 changesetIntersectionObserver.observe(this);
113 function toggleChangesetHighlight(id, state) {
114 changesetsLayer.toggleChangesetHighlight(id, state);
115 $("#sidebar_content .changesets ol li").removeClass("selected");
117 $("#changeset_" + id).addClass("selected");
121 function displayFirstChangesets(html) {
122 $("#sidebar_content .changesets").html(html);
124 $("#sidebar_content .changesets ol")
125 .before($("<div class='changeset-color-hint-bar opacity-75 sticky-top changeset-above-sidebar-viewport'>"))
126 .after($("<div class='changeset-color-hint-bar opacity-75 sticky-bottom changeset-below-sidebar-viewport'>"));
128 if (location.pathname === "/history") {
129 setPaginationMapHashes();
133 function displayMoreChangesets(div, html) {
134 const sidebar = $("#sidebar")[0];
135 const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
137 const oldList = $("#sidebar_content .changesets ol");
139 div.replaceWith(html);
141 const prevNewList = oldList.prevAll("ol");
142 if (prevNewList.length) {
143 prevNewList.next(".changeset_more").remove();
144 prevNewList.children().prependTo(oldList);
145 prevNewList.remove();
147 // restore scroll position only if prepending
148 sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
151 const nextNewList = oldList.nextAll("ol");
152 if (nextNewList.length) {
153 nextNewList.prev(".changeset_more").remove();
154 nextNewList.children().appendTo(oldList);
155 nextNewList.remove();
158 if (location.pathname === "/history") {
159 setPaginationMapHashes();
163 function setPaginationMapHashes() {
164 $("#sidebar .pagination a").each(function () {
165 $(this).prop("hash", OSM.formatHash(map));
169 function loadFirstChangesets() {
170 const data = new URLSearchParams();
172 disableChangesetIntersectionObserver();
174 if (location.pathname === "/history") {
175 setBboxFetchData(data);
176 const feedLink = $("link[type=\"application/atom+xml\"]"),
177 feedHref = feedLink.attr("href").split("?")[0];
178 feedLink.attr("href", feedHref + "?" + data);
181 setListFetchData(data, location);
183 fetch(location.pathname + "?" + data)
184 .then(response => response.text())
185 .then(function (html) {
186 displayFirstChangesets(html);
187 enableChangesetIntersectionObserver();
189 if (data.has("before")) {
190 const [firstItem] = $("#sidebar_content .changesets ol").children().first();
191 firstItem?.scrollIntoView();
192 } else if (data.has("after")) {
193 const [lastItem] = $("#sidebar_content .changesets ol").children().last();
194 lastItem?.scrollIntoView(false);
196 const [sidebar] = $("#sidebar");
197 sidebar.scrollTop = 0;
204 function loadMoreChangesets(e) {
208 const div = $(this).parents(".changeset_more");
210 div.find(".pagination").addClass("invisible");
211 div.find("[hidden]").prop("hidden", false);
213 const data = new URLSearchParams();
215 if (location.pathname === "/history") {
216 setBboxFetchData(data);
219 const url = new URL($(this).attr("href"), location);
220 setListFetchData(data, url);
222 fetch(url.pathname + "?" + data)
223 .then(response => response.text())
224 .then(function (html) {
225 displayMoreChangesets(div, html);
226 enableChangesetIntersectionObserver();
232 function setBboxFetchData(data) {
233 const crs = map.options.crs;
234 const sw = map.getBounds().getSouthWest();
235 const ne = map.getBounds().getNorthEast();
236 const swClamped = crs.unproject(crs.project(sw));
237 const neClamped = crs.unproject(crs.project(ne));
239 if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
240 data.set("bbox", map.getBounds().toBBoxString());
244 function setListFetchData(data, url) {
245 const params = new URLSearchParams(url.search);
247 data.set("list", "1");
249 if (params.has("before")) {
250 data.set("before", params.get("before"));
252 if (params.has("after")) {
253 data.set("after", params.get("after"));
257 function moveEndListener() {
258 if (location.pathname === "/history") {
259 OSM.router.replace("/history" + window.location.hash);
260 loadFirstChangesets();
262 $("#sidebar_content .changesets ol li").removeClass("selected");
263 changesetsLayer.updateChangesetsGeometry(map);
267 function zoomEndListener() {
268 $("#sidebar_content .changesets ol li").removeClass("selected");
269 changesetsLayer.updateChangesetsGeometry(map);
272 function updateMap() {
273 const changesets = $("[data-changeset]").map(function (index, element) {
274 return $(element).data("changeset");
275 }).get().filter(function (changeset) {
276 return changeset.bbox;
279 changesetsLayer.updateChangesets(map, changesets);
281 if (location.pathname !== "/history") {
282 const bounds = changesetsLayer.getBounds();
283 if (bounds.isValid()) map.fitBounds(bounds);
287 page.pushstate = page.popstate = function (path) {
288 OSM.loadSidebarContent(path, page.load);
291 page.load = function () {
292 map.addLayer(changesetsLayer);
293 map.on("moveend", moveEndListener);
294 map.on("zoomend", zoomEndListener);
295 loadFirstChangesets();
298 page.unload = function () {
299 map.removeLayer(changesetsLayer);
300 map.off("moveend", moveEndListener);
301 map.off("zoomend", zoomEndListener);
302 disableChangesetIntersectionObserver();