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 highlightChangeset($(this).data("changeset").id);
12 .on("mouseout", "[data-changeset]", function () {
13 unHighlightChangeset($(this).data("changeset").id);
16 const changesetsLayer = new OSM.HistoryChangesetsLayer()
17 .on("mouseover", function (e) {
18 highlightChangeset(e.layer.id);
20 .on("mouseout", function (e) {
21 unHighlightChangeset(e.layer.id);
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 ignoreIntersectionEvents = true;
42 changesetIntersectionObserver = new IntersectionObserver((entries) => {
43 if (ignoreIntersectionEvents) {
44 ignoreIntersectionEvents = false;
48 let closestTargetToTop,
49 closestDistanceToTop = Infinity,
50 closestTargetToBottom,
51 closestDistanceToBottom = Infinity;
53 for (const entry of entries) {
54 if (entry.isIntersecting) continue;
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;
62 if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
63 closestDistanceToBottom = distanceToBottom;
64 closestTargetToBottom = entry.target;
68 if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
69 const id = $(closestTargetToTop).data("changeset")?.id;
71 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
73 } else if (closestTargetToBottom) {
74 const id = $(closestTargetToBottom).data("changeset")?.id;
76 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
79 }, { root: $("#sidebar")[0] });
81 $("#sidebar_content .changesets ol").children().each(function () {
82 changesetIntersectionObserver.observe(this);
86 function highlightChangeset(id) {
87 changesetsLayer.highlightChangeset(id);
88 $("#changeset_" + id).addClass("selected");
91 function unHighlightChangeset(id) {
92 changesetsLayer.unHighlightChangeset(id);
93 $("#changeset_" + id).removeClass("selected");
96 function clickChangeset(id, e) {
97 $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
100 function displayFirstChangesets(html) {
101 $("#sidebar_content .changesets").html(html);
103 if (location.pathname === "/history") {
104 setPaginationMapHashes();
108 function displayMoreChangesets(div, html) {
109 const sidebar = $("#sidebar")[0];
110 const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
112 const oldList = $("#sidebar_content .changesets ol");
114 div.replaceWith(html);
116 const prevNewList = oldList.prevAll("ol");
117 if (prevNewList.length) {
118 prevNewList.next(".changeset_more").remove();
119 prevNewList.children().prependTo(oldList);
120 prevNewList.remove();
122 // restore scroll position only if prepending
123 sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
126 const nextNewList = oldList.nextAll("ol");
127 if (nextNewList.length) {
128 nextNewList.prev(".changeset_more").remove();
129 nextNewList.children().appendTo(oldList);
130 nextNewList.remove();
133 if (location.pathname === "/history") {
134 setPaginationMapHashes();
138 function setPaginationMapHashes() {
139 $("#sidebar .pagination a").each(function () {
140 $(this).prop("hash", OSM.formatHash({
141 center: map.getCenter(),
147 function loadFirstChangesets() {
148 const data = new URLSearchParams();
150 disableChangesetIntersectionObserver();
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);
159 setListFetchData(data, location);
161 fetch(location.pathname + "?" + data)
162 .then(response => response.text())
163 .then(function (html) {
164 displayFirstChangesets(html);
165 enableChangesetIntersectionObserver();
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);
174 const [sidebar] = $("#sidebar");
175 sidebar.scrollTop = 0;
182 function loadMoreChangesets(e) {
186 const div = $(this).parents(".changeset_more");
188 div.find(".pagination").addClass("invisible");
189 div.find("[hidden]").prop("hidden", false);
191 const data = new URLSearchParams();
193 if (location.pathname === "/history") {
194 setBboxFetchData(data);
197 const url = new URL($(this).attr("href"), location);
198 setListFetchData(data, url);
200 fetch(url.pathname + "?" + data)
201 .then(response => response.text())
202 .then(function (html) {
203 displayMoreChangesets(div, html);
204 enableChangesetIntersectionObserver();
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));
217 if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
218 data.set("bbox", map.getBounds().toBBoxString());
222 function setListFetchData(data, url) {
223 const params = new URLSearchParams(url.search);
225 data.set("list", "1");
227 if (params.has("before")) {
228 data.set("before", params.get("before"));
230 if (params.has("after")) {
231 data.set("after", params.get("after"));
235 function moveEndListener() {
236 if (location.pathname === "/history") {
237 OSM.router.replace("/history" + window.location.hash);
238 loadFirstChangesets();
240 changesetsLayer.updateChangesetsPositions(map);
244 function zoomEndListener() {
245 changesetsLayer.updateChangesetShapes(map);
248 function updateMap() {
249 const changesets = $("[data-changeset]").map(function (index, element) {
250 return $(element).data("changeset");
251 }).get().filter(function (changeset) {
252 return changeset.bbox;
255 changesetsLayer.updateChangesets(map, changesets);
257 if (location.pathname !== "/history") {
258 const bounds = changesetsLayer.getBounds();
259 if (bounds.isValid()) map.fitBounds(bounds);
263 page.pushstate = page.popstate = function (path) {
264 OSM.loadSidebarContent(path, page.load);
267 page.load = function () {
268 map.addLayer(changesetsLayer);
269 map.on("moveend", moveEndListener);
270 map.on("zoomend", zoomEndListener);
271 loadFirstChangesets();
274 page.unload = function () {
275 map.removeLayer(changesetsLayer);
276 map.off("moveend", moveEndListener);
277 map.off("zoomend", zoomEndListener);
278 disableChangesetIntersectionObserver();