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);
16 const changesetsLayer = new OSM.HistoryChangesetsLayer()
17 .on("mouseover", function (e) {
18 toggleChangesetHighlight(e.layer.id, true);
20 .on("mouseout", function (e) {
21 toggleChangesetHighlight(e.layer.id, false);
23 .on("click", function (e) {
24 clickChangeset(e.layer.id, e.originalEvent);
27 let changesetIntersectionObserver;
29 function disableChangesetIntersectionObserver() {
30 if (changesetIntersectionObserver) {
31 changesetIntersectionObserver.disconnect();
32 changesetIntersectionObserver = null;
36 function enableChangesetIntersectionObserver() {
37 disableChangesetIntersectionObserver();
38 if (!window.IntersectionObserver) return;
40 let keepInitialLocation = true;
41 let itemsInViewport = $();
43 changesetIntersectionObserver = new IntersectionObserver((entries) => {
44 let closestTargetToTop,
45 closestDistanceToTop = Infinity,
46 closestTargetToBottom,
47 closestDistanceToBottom = Infinity;
49 for (const entry of entries) {
50 const id = $(entry.target).data("changeset")?.id;
52 if (entry.isIntersecting) {
53 itemsInViewport = itemsInViewport.add(entry.target);
54 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 0);
57 itemsInViewport = itemsInViewport.not(entry.target);
60 const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
61 const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
63 if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
64 closestDistanceToTop = distanceToTop;
65 closestTargetToTop = entry.target;
67 if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
68 closestDistanceToBottom = distanceToBottom;
69 closestTargetToBottom = entry.target;
73 itemsInViewport.first().prevAll().each(function () {
74 const id = $(this).data("changeset")?.id;
75 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 1);
77 itemsInViewport.last().nextAll().each(function () {
78 const id = $(this).data("changeset")?.id;
79 if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, -1);
82 changesetsLayer.reorderChangesets();
84 if (keepInitialLocation) {
85 keepInitialLocation = false;
89 if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
90 const id = $(closestTargetToTop).data("changeset")?.id;
92 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
94 } else if (closestTargetToBottom) {
95 const id = $(closestTargetToBottom).data("changeset")?.id;
97 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
100 }, { root: $("#sidebar")[0] });
102 $("#sidebar_content .changesets ol").children().each(function () {
103 changesetIntersectionObserver.observe(this);
107 function toggleChangesetHighlight(id, state) {
108 changesetsLayer.toggleChangesetHighlight(id, state);
109 $("#changeset_" + id).toggleClass("selected", state);
112 function clickChangeset(id, e) {
113 $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
116 function displayFirstChangesets(html) {
117 $("#sidebar_content .changesets").html(html);
119 $("#sidebar_content .changesets ol")
120 .before($("<div class='changeset-color-hint-bar opacity-75 sticky-top changeset-above-sidebar-viewport'>"))
121 .after($("<div class='changeset-color-hint-bar opacity-75 sticky-bottom changeset-below-sidebar-viewport'>"));
123 if (location.pathname === "/history") {
124 setPaginationMapHashes();
128 function displayMoreChangesets(div, html) {
129 const sidebar = $("#sidebar")[0];
130 const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
132 const oldList = $("#sidebar_content .changesets ol");
134 div.replaceWith(html);
136 const prevNewList = oldList.prevAll("ol");
137 if (prevNewList.length) {
138 prevNewList.next(".changeset_more").remove();
139 prevNewList.children().prependTo(oldList);
140 prevNewList.remove();
142 // restore scroll position only if prepending
143 sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
146 const nextNewList = oldList.nextAll("ol");
147 if (nextNewList.length) {
148 nextNewList.prev(".changeset_more").remove();
149 nextNewList.children().appendTo(oldList);
150 nextNewList.remove();
153 if (location.pathname === "/history") {
154 setPaginationMapHashes();
158 function setPaginationMapHashes() {
159 $("#sidebar .pagination a").each(function () {
160 $(this).prop("hash", OSM.formatHash({
161 center: map.getCenter(),
167 function loadFirstChangesets() {
168 const data = new URLSearchParams();
170 disableChangesetIntersectionObserver();
172 if (location.pathname === "/history") {
173 setBboxFetchData(data);
174 const feedLink = $("link[type=\"application/atom+xml\"]"),
175 feedHref = feedLink.attr("href").split("?")[0];
176 feedLink.attr("href", feedHref + "?" + data);
179 setListFetchData(data, location);
181 fetch(location.pathname + "?" + data)
182 .then(response => response.text())
183 .then(function (html) {
184 displayFirstChangesets(html);
185 enableChangesetIntersectionObserver();
187 if (data.has("before")) {
188 const [firstItem] = $("#sidebar_content .changesets ol").children().first();
189 firstItem?.scrollIntoView();
190 } else if (data.has("after")) {
191 const [lastItem] = $("#sidebar_content .changesets ol").children().last();
192 lastItem?.scrollIntoView(false);
194 const [sidebar] = $("#sidebar");
195 sidebar.scrollTop = 0;
202 function loadMoreChangesets(e) {
206 const div = $(this).parents(".changeset_more");
208 div.find(".pagination").addClass("invisible");
209 div.find("[hidden]").prop("hidden", false);
211 const data = new URLSearchParams();
213 if (location.pathname === "/history") {
214 setBboxFetchData(data);
217 const url = new URL($(this).attr("href"), location);
218 setListFetchData(data, url);
220 fetch(url.pathname + "?" + data)
221 .then(response => response.text())
222 .then(function (html) {
223 displayMoreChangesets(div, html);
224 enableChangesetIntersectionObserver();
230 function setBboxFetchData(data) {
231 const crs = map.options.crs;
232 const sw = map.getBounds().getSouthWest();
233 const ne = map.getBounds().getNorthEast();
234 const swClamped = crs.unproject(crs.project(sw));
235 const neClamped = crs.unproject(crs.project(ne));
237 if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
238 data.set("bbox", map.getBounds().toBBoxString());
242 function setListFetchData(data, url) {
243 const params = new URLSearchParams(url.search);
245 data.set("list", "1");
247 if (params.has("before")) {
248 data.set("before", params.get("before"));
250 if (params.has("after")) {
251 data.set("after", params.get("after"));
255 function moveEndListener() {
256 if (location.pathname === "/history") {
257 OSM.router.replace("/history" + window.location.hash);
258 loadFirstChangesets();
260 changesetsLayer.updateChangesetLocations(map);
264 function zoomEndListener() {
265 changesetsLayer.updateChangesetShapes(map);
268 function updateMap() {
269 const changesets = $("[data-changeset]").map(function (index, element) {
270 return $(element).data("changeset");
271 }).get().filter(function (changeset) {
272 return changeset.bbox;
275 changesetsLayer.updateChangesets(map, changesets);
277 if (location.pathname !== "/history") {
278 const bounds = changesetsLayer.getBounds();
279 if (bounds.isValid()) map.fitBounds(bounds);
283 page.pushstate = page.popstate = function (path) {
284 OSM.loadSidebarContent(path, page.load);
287 page.load = function () {
288 map.addLayer(changesetsLayer);
289 map.on("moveend", moveEndListener);
290 map.on("zoomend", zoomEndListener);
291 loadFirstChangesets();
294 page.unload = function () {
295 map.removeLayer(changesetsLayer);
296 map.off("moveend", moveEndListener);
297 map.off("zoomend", zoomEndListener);
298 disableChangesetIntersectionObserver();