1 //= require jquery-simulate/jquery.simulate
3 OSM.History = function (map) {
7 .on("click", ".changeset_more a", loadMore)
8 .on("mouseover", "[data-changeset]", function () {
9 highlightChangeset($(this).data("changeset").id);
11 .on("mouseout", "[data-changeset]", function () {
12 unHighlightChangeset($(this).data("changeset").id);
15 const group = L.featureGroup()
16 .on("mouseover", function (e) {
17 highlightChangeset(e.layer.id);
19 .on("mouseout", function (e) {
20 unHighlightChangeset(e.layer.id);
22 .on("click", function (e) {
23 clickChangeset(e.layer.id, e.originalEvent);
26 group.getLayerId = function (layer) {
30 let changesetIntersectionObserver;
32 function disableChangesetIntersectionObserver() {
33 if (changesetIntersectionObserver) {
34 changesetIntersectionObserver.disconnect();
35 changesetIntersectionObserver = null;
39 function enableChangesetIntersectionObserver() {
40 disableChangesetIntersectionObserver();
41 if (!window.IntersectionObserver) return;
43 let ignoreIntersectionEvents = true;
45 changesetIntersectionObserver = new IntersectionObserver((entries) => {
46 if (ignoreIntersectionEvents) {
47 ignoreIntersectionEvents = false;
51 let closestTargetToTop,
52 closestDistanceToTop = Infinity,
53 closestTargetToBottom,
54 closestDistanceToBottom = Infinity;
56 for (const entry of entries) {
57 if (entry.isIntersecting) continue;
59 const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
60 const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
61 if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
62 closestDistanceToTop = distanceToTop;
63 closestTargetToTop = entry.target;
65 if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
66 closestDistanceToBottom = distanceToBottom;
67 closestTargetToBottom = entry.target;
71 if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
72 const id = $(closestTargetToTop).data("changeset")?.id;
74 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
76 } else if (closestTargetToBottom) {
77 const id = $(closestTargetToBottom).data("changeset")?.id;
79 OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
82 }, { root: $("#sidebar")[0] });
84 $("#sidebar_content .changesets ol").children().each(function () {
85 changesetIntersectionObserver.observe(this);
89 function highlightChangeset(id) {
90 const layer = group.getLayer(id);
91 if (layer) layer.setStyle({ fillOpacity: 0.3, color: "#FF6600", weight: 3 });
92 $("#changeset_" + id).addClass("selected");
95 function unHighlightChangeset(id) {
96 const layer = group.getLayer(id);
97 if (layer) layer.setStyle({ fillOpacity: 0, color: "#FF9500", weight: 2 });
98 $("#changeset_" + id).removeClass("selected");
101 function clickChangeset(id, e) {
102 $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
105 function displayFirstChangesets(html) {
106 $("#sidebar_content .changesets").html(html);
109 function displayMoreChangesets(div, html) {
110 const sidebar = $("#sidebar")[0];
111 const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
113 const oldList = $("#sidebar_content .changesets ol");
115 div.replaceWith(html);
117 const prevNewList = oldList.prevAll("ol");
118 if (prevNewList.length) {
119 prevNewList.next(".changeset_more").remove();
120 prevNewList.children().prependTo(oldList);
121 prevNewList.remove();
123 // restore scroll position only if prepending
124 sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
127 const nextNewList = oldList.nextAll("ol");
128 if (nextNewList.length) {
129 nextNewList.prev(".changeset_more").remove();
130 nextNewList.children().appendTo(oldList);
131 nextNewList.remove();
136 const data = new URLSearchParams();
137 const params = new URLSearchParams(location.search);
139 disableChangesetIntersectionObserver();
141 if (location.pathname === "/history") {
142 data.set("bbox", map.getBounds().wrap().toBBoxString());
143 const feedLink = $("link[type=\"application/atom+xml\"]"),
144 feedHref = feedLink.attr("href").split("?")[0];
145 feedLink.attr("href", feedHref + "?" + data);
148 data.set("list", "1");
150 if (params.has("before")) {
151 data.set("before", params.get("before"));
153 if (params.has("after")) {
154 data.set("after", params.get("after"));
157 fetch(location.pathname + "?" + data)
158 .then(response => response.text())
159 .then(function (html) {
160 displayFirstChangesets(html);
161 enableChangesetIntersectionObserver();
163 if (params.has("before")) {
164 const [firstItem] = $("#sidebar_content .changesets ol").children().first();
165 firstItem?.scrollIntoView();
167 if (params.has("after")) {
168 const [lastItem] = $("#sidebar_content .changesets ol").children().last();
169 lastItem?.scrollIntoView(false);
176 function loadMore(e) {
180 const div = $(this).parents(".changeset_more");
183 div.find(".loader").show();
185 $.get($(this).attr("href"), function (html) {
186 displayMoreChangesets(div, html);
187 enableChangesetIntersectionObserver();
194 function updateBounds() {
197 for (const changeset of changesets) {
198 const bottomLeft = map.project(L.latLng(changeset.bbox.minlat, changeset.bbox.minlon)),
199 topRight = map.project(L.latLng(changeset.bbox.maxlat, changeset.bbox.maxlon)),
200 width = topRight.x - bottomLeft.x,
201 height = bottomLeft.y - topRight.y,
202 minSize = 20; // Min width/height of changeset in pixels
204 if (width < minSize) {
205 bottomLeft.x -= ((minSize - width) / 2);
206 topRight.x += ((minSize - width) / 2);
209 if (height < minSize) {
210 bottomLeft.y += ((minSize - height) / 2);
211 topRight.y -= ((minSize - height) / 2);
214 changeset.bounds = L.latLngBounds(map.unproject(bottomLeft),
215 map.unproject(topRight));
218 changesets.sort(function (a, b) {
219 return b.bounds.getSize() - a.bounds.getSize();
222 for (const changeset of changesets) {
223 const rect = L.rectangle(changeset.bounds,
224 { weight: 2, color: "#FF9500", opacity: 1, fillColor: "#FFFFAF", fillOpacity: 0 });
225 rect.id = changeset.id;
230 function updateMap() {
231 changesets = $("[data-changeset]").map(function (index, element) {
232 return $(element).data("changeset");
233 }).get().filter(function (changeset) {
234 return changeset.bbox;
239 if (location.pathname !== "/history") {
240 const bounds = group.getBounds();
241 if (bounds.isValid()) map.fitBounds(bounds);
245 page.pushstate = page.popstate = function (path) {
246 OSM.loadSidebarContent(path, page.load);
249 page.load = function () {
252 if (location.pathname === "/history") {
253 map.on("moveend", update);
256 map.on("zoomend", updateBounds);
261 page.unload = function () {
262 map.removeLayer(group);
263 map.off("moveend", update);
264 map.off("zoomend", updateBounds);
265 disableChangesetIntersectionObserver();