]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'upstream/pull/5832'
authorTom Hughes <tom@compton.nu>
Thu, 10 Apr 2025 16:09:05 +0000 (17:09 +0100)
committerTom Hughes <tom@compton.nu>
Thu, 10 Apr 2025 16:09:05 +0000 (17:09 +0100)
20 files changed:
app/assets/javascripts/index/directions-route-output.js [new file with mode: 0644]
app/assets/javascripts/index/directions.js
app/assets/javascripts/index/directions/fossgis_osrm.js
app/assets/javascripts/index/directions/fossgis_valhalla.js
app/assets/javascripts/index/directions/graphhopper.js
app/assets/javascripts/index/history-changesets-layer.js [new file with mode: 0644]
app/assets/javascripts/index/history.js
app/controllers/changesets_controller.rb
config/locales/he.yml
config/locales/mk.yml
config/locales/ne.yml
config/locales/nl.yml
config/locales/pt.yml
config/locales/uk.yml
config/locales/zh-TW.yml
test/controllers/api/changesets_controller_test.rb
test/controllers/changesets_controller_test.rb
test/factories/changesets.rb
test/system/browse_comment_links_test.rb
test/system/history_test.rb

diff --git a/app/assets/javascripts/index/directions-route-output.js b/app/assets/javascripts/index/directions-route-output.js
new file mode 100644 (file)
index 0000000..84bcc87
--- /dev/null
@@ -0,0 +1,142 @@
+OSM.DirectionsRouteOutput = function (map) {
+  const popup = L.popup({ autoPanPadding: [100, 100] });
+
+  const polyline = L.polyline([], {
+    color: "#03f",
+    opacity: 0.3,
+    weight: 10
+  });
+
+  const highlight = L.polyline([], {
+    color: "#ff0",
+    opacity: 0.5,
+    weight: 12
+  });
+
+  let downloadURL = null;
+
+  function formatTotalDistance(m) {
+    if (m < 1000) {
+      return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
+    } else if (m < 10000) {
+      return OSM.i18n.t("javascripts.directions.distance_km", { distance: (m / 1000.0).toFixed(1) });
+    } else {
+      return OSM.i18n.t("javascripts.directions.distance_km", { distance: Math.round(m / 1000) });
+    }
+  }
+
+  function formatStepDistance(m) {
+    if (m < 5) {
+      return "";
+    } else if (m < 200) {
+      return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 10) * 10) });
+    } else if (m < 1500) {
+      return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 100) * 100) });
+    } else if (m < 5000) {
+      return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 100) / 10) });
+    } else {
+      return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 1000)) });
+    }
+  }
+
+  function formatHeight(m) {
+    return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
+  }
+
+  function formatTime(s) {
+    let m = Math.round(s / 60);
+    const h = Math.floor(m / 60);
+    m -= h * 60;
+    return h + ":" + (m < 10 ? "0" : "") + m;
+  }
+
+  const routeOutput = {};
+
+  routeOutput.write = function (content, route) {
+    polyline
+      .setLatLngs(route.line)
+      .addTo(map);
+
+    const distanceText = $("<p>").append(
+      OSM.i18n.t("javascripts.directions.distance") + ": " + formatTotalDistance(route.distance) + ". " +
+      OSM.i18n.t("javascripts.directions.time") + ": " + formatTime(route.time) + ".");
+    if (typeof route.ascend !== "undefined" && typeof route.descend !== "undefined") {
+      distanceText.append(
+        $("<br>"),
+        OSM.i18n.t("javascripts.directions.ascend") + ": " + formatHeight(route.ascend) + ". " +
+        OSM.i18n.t("javascripts.directions.descend") + ": " + formatHeight(route.descend) + ".");
+    }
+
+    const turnByTurnTable = $("<table class='table table-hover table-sm mb-3'>")
+      .append($("<tbody>"));
+
+    content
+      .empty()
+      .append(
+        distanceText,
+        turnByTurnTable
+      );
+
+    for (const [i, [direction, instruction, dist, lineseg]] of route.steps.entries()) {
+      const row = $("<tr class='turn'/>").appendTo(turnByTurnTable);
+
+      if (direction) {
+        row.append("<td class='border-0'><svg width='20' height='20' class='d-block'><use href='#routing-sprite-" + direction + "' /></svg></td>");
+      } else {
+        row.append("<td class='border-0'>");
+      }
+      row.append(`<td><b>${i + 1}.</b> ${instruction}`);
+      row.append("<td class='distance text-body-secondary text-end'>" + formatStepDistance(dist));
+
+      row.on("click", function () {
+        popup
+          .setLatLng(lineseg[0])
+          .setContent(`<p><b>${i + 1}.</b> ${instruction}</p>`)
+          .openOn(map);
+      });
+
+      row
+        .on("mouseenter", function () {
+          highlight
+            .setLatLngs(lineseg)
+            .addTo(map);
+        })
+        .on("mouseleave", function () {
+          map.removeLayer(highlight);
+        });
+    }
+
+    const blob = new Blob([JSON.stringify(polyline.toGeoJSON())], { type: "application/json" });
+    URL.revokeObjectURL(downloadURL);
+    downloadURL = URL.createObjectURL(blob);
+
+    content.append(`<p class="text-center"><a href="${downloadURL}" download="${
+      OSM.i18n.t("javascripts.directions.filename")
+    }">${
+      OSM.i18n.t("javascripts.directions.download")
+    }</a></p>`);
+
+    content.append("<p class=\"text-center\">" +
+      OSM.i18n.t("javascripts.directions.instructions.courtesy", {
+        link: `<a href="${route.creditlink}" target="_blank">${route.credit}</a>`
+      }) +
+      "</p>");
+  };
+
+  routeOutput.fit = function () {
+    map.fitBounds(polyline.getBounds().pad(0.05));
+  };
+
+  routeOutput.isVisible = function () {
+    return map.hasLayer(polyline);
+  };
+
+  routeOutput.remove = function (content) {
+    content.empty();
+    map
+      .removeLayer(popup)
+      .removeLayer(polyline);
+  };
+
+  return routeOutput;
+};
index 45ff960e231acd6b488dc0611f13b41c926ffa8b..c5d5cce1589a0d63f6698374546dccedf48eb558 100644 (file)
@@ -1,4 +1,5 @@
 //= require ./directions-endpoint
+//= require ./directions-route-output
 //= require_self
 //= require_tree ./directions
 
@@ -7,22 +8,10 @@ OSM.Directions = function (map) {
   let lastLocation = [];
   let chosenEngine;
 
-  const popup = L.popup({ autoPanPadding: [100, 100] });
-
-  const polyline = L.polyline([], {
-    color: "#03f",
-    opacity: 0.3,
-    weight: 10
-  });
-
-  const highlight = L.polyline([], {
-    color: "#ff0",
-    opacity: 0.5,
-    weight: 12
-  });
+  const routeOutput = OSM.DirectionsRouteOutput(map);
 
   const endpointDragCallback = function (dragging) {
-    if (!map.hasLayer(polyline)) return;
+    if (!routeOutput.isVisible()) return;
     if (dragging && !chosenEngine.draggable) return;
     if (dragging && controller) return;
 
@@ -37,8 +26,6 @@ OSM.Directions = function (map) {
     OSM.DirectionsEndpoint(map, $("input[name='route_to']"), { icon: "MARKER_RED" }, endpointDragCallback, endpointChangeCallback)
   ];
 
-  let downloadURL = null;
-
   const expiry = new Date();
   expiry.setYear(expiry.getFullYear() + 10);
 
@@ -70,41 +57,6 @@ OSM.Directions = function (map) {
     OSM.router.route("/" + OSM.formatHash(map));
   });
 
-  function formatTotalDistance(m) {
-    if (m < 1000) {
-      return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
-    } else if (m < 10000) {
-      return OSM.i18n.t("javascripts.directions.distance_km", { distance: (m / 1000.0).toFixed(1) });
-    } else {
-      return OSM.i18n.t("javascripts.directions.distance_km", { distance: Math.round(m / 1000) });
-    }
-  }
-
-  function formatStepDistance(m) {
-    if (m < 5) {
-      return "";
-    } else if (m < 200) {
-      return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 10) * 10) });
-    } else if (m < 1500) {
-      return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 100) * 100) });
-    } else if (m < 5000) {
-      return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 100) / 10) });
-    } else {
-      return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 1000)) });
-    }
-  }
-
-  function formatHeight(m) {
-    return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
-  }
-
-  function formatTime(s) {
-    let m = Math.round(s / 60);
-    const h = Math.floor(m / 60);
-    m -= h * 60;
-    return h + ":" + (m < 10 ? "0" : "") + m;
-  }
-
   function setEngine(id) {
     const engines = OSM.Directions.engines;
     const desired = engines.find(engine => engine.id === id);
@@ -155,78 +107,12 @@ OSM.Directions = function (map) {
     map.setSidebarOverlaid(false);
     controller = new AbortController();
     chosenEngine.getRoute(points, controller.signal).then(function (route) {
-      polyline
-        .setLatLngs(route.line)
-        .addTo(map);
-
+      routeOutput.write($("#directions_content"), route);
       if (fitRoute) {
-        map.fitBounds(polyline.getBounds().pad(0.05));
+        routeOutput.fit();
       }
-
-      const distanceText = $("<p>").append(
-        OSM.i18n.t("javascripts.directions.distance") + ": " + formatTotalDistance(route.distance) + ". " +
-        OSM.i18n.t("javascripts.directions.time") + ": " + formatTime(route.time) + ".");
-      if (typeof route.ascend !== "undefined" && typeof route.descend !== "undefined") {
-        distanceText.append(
-          $("<br>"),
-          OSM.i18n.t("javascripts.directions.ascend") + ": " + formatHeight(route.ascend) + ". " +
-          OSM.i18n.t("javascripts.directions.descend") + ": " + formatHeight(route.descend) + ".");
-      }
-
-      const turnByTurnTable = $("<table class='table table-hover table-sm mb-3'>")
-        .append($("<tbody>"));
-
-      $("#directions_content")
-        .empty()
-        .append(
-          distanceText,
-          turnByTurnTable
-        );
-
-      // Add each row
-      turnByTurnTable.append(route.steps.map(([direction, instruction, dist, lineseg], i) => {
-        const row = $("<tr class='turn'/>");
-        if (direction) {
-          row.append("<td class='border-0'><svg width='20' height='20' class='d-block'><use href='#routing-sprite-" + direction + "' /></svg></td>");
-        } else {
-          row.append("<td class='border-0'>");
-        }
-        row.append(`<td><b>${i + 1}.</b> ${instruction}`);
-        row.append("<td class='distance text-body-secondary text-end'>" + formatStepDistance(dist));
-
-        row.on("click", function () {
-          popup
-            .setLatLng(lineseg[0])
-            .setContent(`<p><b>${i + 1}.</b> ${instruction}</p>`)
-            .openOn(map);
-        });
-
-        row.hover(function () {
-          highlight
-            .setLatLngs(lineseg)
-            .addTo(map);
-        }, function () {
-          map.removeLayer(highlight);
-        });
-
-        return row;
-      }));
-
-      const blob = new Blob([JSON.stringify(polyline.toGeoJSON())], { type: "application/json" });
-      URL.revokeObjectURL(downloadURL);
-      downloadURL = URL.createObjectURL(blob);
-
-      $("#directions_content").append(`<p class="text-center"><a href="${downloadURL}" download="${
-        OSM.i18n.t("javascripts.directions.filename")
-      }">${
-        OSM.i18n.t("javascripts.directions.download")
-      }</a></p>`);
-
-      $("#directions_content").append("<p class=\"text-center\">" +
-        OSM.i18n.t("javascripts.directions.instructions.courtesy", { link: chosenEngine.creditline }) +
-        "</p>");
     }).catch(function () {
-      map.removeLayer(polyline);
+      routeOutput.remove($("#directions_content"));
       if (reportErrors) {
         $("#directions_content").html("<div class=\"alert alert-danger\">" + OSM.i18n.t("javascripts.directions.errors.no_route") + "</div>");
       }
@@ -235,11 +121,9 @@ OSM.Directions = function (map) {
     });
   }
 
-  function hideRoute(e) {
+  function closeButtonListener(e) {
     e.stopPropagation();
-    map.removeLayer(polyline);
-    $("#directions_content").html("");
-    popup.close();
+    routeOutput.remove($("#directions_content"));
     map.setSidebarOverlaid(true);
     // TODO: collapse width of sidebar back to previous
   }
@@ -301,7 +185,7 @@ OSM.Directions = function (map) {
   }
 
   function enableListeners() {
-    $("#sidebar .sidebar-close-controls button").on("click", hideRoute);
+    $("#sidebar .sidebar-close-controls button").on("click", closeButtonListener);
 
     $("#map").on("dragend dragover", function (e) {
       e.preventDefault();
@@ -357,7 +241,7 @@ OSM.Directions = function (map) {
     $(".search_form").show();
     $(".directions_form").hide();
 
-    $("#sidebar .sidebar-close-controls button").off("click", hideRoute);
+    $("#sidebar .sidebar-close-controls button").off("click", closeButtonListener);
     $("#map").off("dragend dragover drop");
     map.off("locationfound", sendstartinglocation);
 
@@ -367,9 +251,7 @@ OSM.Directions = function (map) {
     endpoints[0].clearValue();
     endpoints[1].clearValue();
 
-    map
-      .removeLayer(popup)
-      .removeLayer(polyline);
+    routeOutput.remove($("#directions_content"));
   };
 
   return page;
index 40181e588e7679382d47f72a397ff3a5c5fd2eae..53043e06d35366df94da0505c333360f68d6bdf4 100644 (file)
         line: steps.flatMap(step => step[3]),
         steps,
         distance: leg.distance,
-        time: leg.duration
+        time: leg.duration,
+        credit: "OSRM (FOSSGIS)",
+        creditlink: "https://routing.openstreetmap.de/about.html"
       };
     }
 
     return {
       mode: modeId,
       provider: "fossgis_osrm",
-      creditline: "<a href=\"https://routing.openstreetmap.de/about.html\" target=\"_blank\">OSRM (FOSSGIS)</a>",
       draggable: true,
 
       getRoute: function (points, signal) {
index 2e12956577339b61fda00d1af843cea1c3890be6..7354e5d65cd52efb12ae431dfcdd108586a2b55c 100644 (file)
         line,
         steps,
         distance: leg.summary.length * 1000,
-        time: leg.summary.time
+        time: leg.summary.time,
+        credit: "Valhalla (FOSSGIS)",
+        creditlink: "https://gis-ops.com/global-open-valhalla-server-online/"
       };
     }
 
     return {
       mode: modeId,
       provider: "fossgis_valhalla",
-      creditline:
-      "<a href='https://gis-ops.com/global-open-valhalla-server-online/' target='_blank'>Valhalla (FOSSGIS)</a>",
       draggable: false,
 
       getRoute: function (points, signal) {
index 182be4497d38da9481f9a7720de66a4adec2da43..dd79a4671f6b90bcc24ea6b1450f961fc3eb7a5c 100644 (file)
         distance: path.distance,
         time: path.time / 1000,
         ascend: path.ascend,
-        descend: path.descend
+        descend: path.descend,
+        credit: "GraphHopper",
+        creditlink: "https://www.graphhopper.com/"
       };
     }
 
     return {
       mode: modeId,
       provider: "graphhopper",
-      creditline: "<a href=\"https://www.graphhopper.com/\" target=\"_blank\">GraphHopper</a>",
       draggable: false,
 
       getRoute: function (points, signal) {
diff --git a/app/assets/javascripts/index/history-changesets-layer.js b/app/assets/javascripts/index/history-changesets-layer.js
new file mode 100644 (file)
index 0000000..37a4587
--- /dev/null
@@ -0,0 +1,76 @@
+OSM.HistoryChangesetsLayer = L.FeatureGroup.extend({
+  _changesets: [],
+
+  updateChangesets: function (map, changesets) {
+    this._changesets = changesets;
+    this.updateChangesetShapes(map);
+  },
+
+  updateChangesetShapes: function (map) {
+    this.clearLayers();
+
+    for (const changeset of this._changesets) {
+      const bottomLeft = map.project(L.latLng(changeset.bbox.minlat, changeset.bbox.minlon)),
+            topRight = map.project(L.latLng(changeset.bbox.maxlat, changeset.bbox.maxlon)),
+            width = topRight.x - bottomLeft.x,
+            height = bottomLeft.y - topRight.y,
+            minSize = 20; // Min width/height of changeset in pixels
+
+      if (width < minSize) {
+        bottomLeft.x -= ((minSize - width) / 2);
+        topRight.x += ((minSize - width) / 2);
+      }
+
+      if (height < minSize) {
+        bottomLeft.y += ((minSize - height) / 2);
+        topRight.y -= ((minSize - height) / 2);
+      }
+
+      changeset.bounds = L.latLngBounds(map.unproject(bottomLeft),
+                                        map.unproject(topRight));
+    }
+
+    this._changesets.sort(function (a, b) {
+      return b.bounds.getSize() - a.bounds.getSize();
+    });
+
+    this.updateChangesetLocations(map);
+
+    for (const changeset of this._changesets) {
+      const rect = L.rectangle(changeset.bounds,
+                               { weight: 2, color: "#FF9500", opacity: 1, fillColor: "#FFFFAF", fillOpacity: 0 });
+      rect.id = changeset.id;
+      rect.addTo(this);
+    }
+  },
+
+  updateChangesetLocations: function (map) {
+    const mapCenterLng = map.getCenter().lng;
+
+    for (const changeset of this._changesets) {
+      const changesetSouthWest = changeset.bounds.getSouthWest();
+      const changesetNorthEast = changeset.bounds.getNorthEast();
+      const changesetCenterLng = (changesetSouthWest.lng + changesetNorthEast.lng) / 2;
+      const shiftInWorldCircumferences = Math.round((changesetCenterLng - mapCenterLng) / 360);
+
+      if (shiftInWorldCircumferences) {
+        changesetSouthWest.lng -= shiftInWorldCircumferences * 360;
+        changesetNorthEast.lng -= shiftInWorldCircumferences * 360;
+
+        this.getLayer(changeset.id)?.setBounds(changeset.bounds);
+      }
+    }
+  },
+
+  highlightChangeset: function (id) {
+    this.getLayer(id)?.setStyle({ fillOpacity: 0.3, color: "#FF6600", weight: 3 });
+  },
+
+  unHighlightChangeset: function (id) {
+    this.getLayer(id)?.setStyle({ fillOpacity: 0, color: "#FF9500", weight: 2 });
+  },
+
+  getLayerId: function (layer) {
+    return layer.id;
+  }
+});
index 1a23ac7b1198188e0c7cfaaa1a10cb09ca9ce1d5..05877784035fba11cb621125ebca2e13f4a6a88b 100644 (file)
@@ -1,4 +1,5 @@
 //= require jquery-simulate/jquery.simulate
+//= require ./history-changesets-layer
 
 OSM.History = function (map) {
   const page = {};
@@ -12,7 +13,7 @@ OSM.History = function (map) {
       unHighlightChangeset($(this).data("changeset").id);
     });
 
-  const group = L.featureGroup()
+  const changesetsLayer = new OSM.HistoryChangesetsLayer()
     .on("mouseover", function (e) {
       highlightChangeset(e.layer.id);
     })
@@ -23,10 +24,6 @@ OSM.History = function (map) {
       clickChangeset(e.layer.id, e.originalEvent);
     });
 
-  group.getLayerId = function (layer) {
-    return layer.id;
-  };
-
   let changesetIntersectionObserver;
 
   function disableChangesetIntersectionObserver() {
@@ -87,14 +84,12 @@ OSM.History = function (map) {
   }
 
   function highlightChangeset(id) {
-    const layer = group.getLayer(id);
-    if (layer) layer.setStyle({ fillOpacity: 0.3, color: "#FF6600", weight: 3 });
+    changesetsLayer.highlightChangeset(id);
     $("#changeset_" + id).addClass("selected");
   }
 
   function unHighlightChangeset(id) {
-    const layer = group.getLayer(id);
-    if (layer) layer.setStyle({ fillOpacity: 0, color: "#FF9500", weight: 2 });
+    changesetsLayer.unHighlightChangeset(id);
     $("#changeset_" + id).removeClass("selected");
   }
 
@@ -220,7 +215,7 @@ OSM.History = function (map) {
     const neClamped = crs.unproject(crs.project(ne));
 
     if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
-      data.set("bbox", map.getBounds().wrap().toBBoxString());
+      data.set("bbox", map.getBounds().toBBoxString());
     }
   }
 
@@ -237,60 +232,30 @@ OSM.History = function (map) {
     }
   }
 
-  function reloadChangesetsBecauseOfMapMovement() {
-    OSM.router.replace("/history" + window.location.hash);
-    loadFirstChangesets();
-  }
-
-  let changesets = [];
-
-  function updateBounds() {
-    group.clearLayers();
-
-    for (const changeset of changesets) {
-      const bottomLeft = map.project(L.latLng(changeset.bbox.minlat, changeset.bbox.minlon)),
-            topRight = map.project(L.latLng(changeset.bbox.maxlat, changeset.bbox.maxlon)),
-            width = topRight.x - bottomLeft.x,
-            height = bottomLeft.y - topRight.y,
-            minSize = 20; // Min width/height of changeset in pixels
-
-      if (width < minSize) {
-        bottomLeft.x -= ((minSize - width) / 2);
-        topRight.x += ((minSize - width) / 2);
-      }
-
-      if (height < minSize) {
-        bottomLeft.y += ((minSize - height) / 2);
-        topRight.y -= ((minSize - height) / 2);
-      }
-
-      changeset.bounds = L.latLngBounds(map.unproject(bottomLeft),
-                                        map.unproject(topRight));
+  function moveEndListener() {
+    if (location.pathname === "/history") {
+      OSM.router.replace("/history" + window.location.hash);
+      loadFirstChangesets();
+    } else {
+      changesetsLayer.updateChangesetsPositions(map);
     }
+  }
 
-    changesets.sort(function (a, b) {
-      return b.bounds.getSize() - a.bounds.getSize();
-    });
-
-    for (const changeset of changesets) {
-      const rect = L.rectangle(changeset.bounds,
-                               { weight: 2, color: "#FF9500", opacity: 1, fillColor: "#FFFFAF", fillOpacity: 0 });
-      rect.id = changeset.id;
-      rect.addTo(group);
-    }
+  function zoomEndListener() {
+    changesetsLayer.updateChangesetShapes(map);
   }
 
   function updateMap() {
-    changesets = $("[data-changeset]").map(function (index, element) {
+    const changesets = $("[data-changeset]").map(function (index, element) {
       return $(element).data("changeset");
     }).get().filter(function (changeset) {
       return changeset.bbox;
     });
 
-    updateBounds();
+    changesetsLayer.updateChangesets(map, changesets);
 
     if (location.pathname !== "/history") {
-      const bounds = group.getBounds();
+      const bounds = changesetsLayer.getBounds();
       if (bounds.isValid()) map.fitBounds(bounds);
     }
   }
@@ -300,21 +265,16 @@ OSM.History = function (map) {
   };
 
   page.load = function () {
-    map.addLayer(group);
-
-    if (location.pathname === "/history") {
-      map.on("moveend", reloadChangesetsBecauseOfMapMovement);
-    }
-
-    map.on("zoomend", updateBounds);
-
+    map.addLayer(changesetsLayer);
+    map.on("moveend", moveEndListener);
+    map.on("zoomend", zoomEndListener);
     loadFirstChangesets();
   };
 
   page.unload = function () {
-    map.removeLayer(group);
-    map.off("moveend", reloadChangesetsBecauseOfMapMovement);
-    map.off("zoomend", updateBounds);
+    map.removeLayer(changesetsLayer);
+    map.off("moveend", moveEndListener);
+    map.off("zoomend", zoomEndListener);
     disableChangesetIntersectionObserver();
   };
 
index 5e79f4da62a70ac42bc4e75203c60a70d073de24..07e7a365f0d1a3884e62a99f3cdb61fc82e64bad 100644 (file)
@@ -54,7 +54,10 @@ class ChangesetsController < ApplicationController
                        changesets.where("false")
                      end
       elsif @params[:bbox]
-        changesets = conditions_bbox(changesets, BoundingBox.from_bbox_params(params))
+        bbox_array = @params[:bbox].split(",").map(&:to_f)
+        raise OSM::APIBadUserInput, "The parameter bbox must be of the form min_lon,min_lat,max_lon,max_lat" unless bbox_array.count == 4
+
+        changesets = conditions_bbox(changesets, *bbox_array)
       elsif @params[:friends] && current_user
         changesets = changesets.where(:user => current_user.followings.identifiable)
       elsif @params[:nearby] && current_user
@@ -113,21 +116,37 @@ class ChangesetsController < ApplicationController
   #------------------------------------------------------------
 
   ##
-  # if a bounding box was specified do some sanity checks.
   # restrict changesets to those enclosed by a bounding box
-  def conditions_bbox(changesets, bbox)
-    if bbox
-      bbox.check_boundaries
-      bbox = bbox.to_scaled
-
-      changesets.where("min_lon < ? and max_lon > ? and min_lat < ? and max_lat > ?",
-                       bbox.max_lon.to_i, bbox.min_lon.to_i,
-                       bbox.max_lat.to_i, bbox.min_lat.to_i)
-    else
+  def conditions_bbox(changesets, min_lon, min_lat, max_lon, max_lat)
+    db_min_lat = (min_lat * GeoRecord::SCALE).to_i
+    db_max_lat = (max_lat * GeoRecord::SCALE).to_i
+    db_min_lon = (wrap_lon(min_lon) * GeoRecord::SCALE).to_i
+    db_max_lon = (wrap_lon(max_lon) * GeoRecord::SCALE).to_i
+
+    changesets = changesets.where("min_lat < ? and max_lat > ?", db_max_lat, db_min_lat)
+
+    if max_lon - min_lon >= 360
+      # the query bbox spans the entire world, therefore no lon checks are necessary
       changesets
+    elsif db_min_lon <= db_max_lon
+      # the normal case when the query bbox doesn't include the antimeridian
+      changesets.where("min_lon < ? and max_lon > ?", db_max_lon, db_min_lon)
+    else
+      # the query bbox includes the antimeridian
+      # this case works as if there are two query bboxes:
+      #   [-180*SCALE .. db_max_lon], [db_min_lon .. 180*SCALE]
+      # it would be necessary to check if changeset bboxes intersect with either of the query bboxes:
+      #   (changesets.min_lon < db_max_lon and changesets.max_lon > -180*SCALE) or (changesets.min_lon < 180*SCALE and changesets.max_lon > db_min_lon)
+      # but the comparisons with -180*SCALE and 180*SCALE are unnecessary:
+      #   (changesets.min_lon < db_max_lon) or (changesets.max_lon > db_min_lon)
+      changesets.where("min_lon < ? or max_lon > ?", db_max_lon, db_min_lon)
     end
   end
 
+  def wrap_lon(lon)
+    ((lon + 180) % 360) - 180
+  end
+
   ##
   # eliminate empty changesets (where the bbox has not been set)
   # this should be applied to all changeset list displays
index 8224becfb0c040d1ff0ffb033bb83f8b9186a6e2..a7c136706889d1b6b667d98610b8af4bdb3e0dad 100644 (file)
@@ -571,7 +571,7 @@ he:
       title: ערכות שינויים
       title_user: ערכות שינויים מאת %{user}
       title_user_link_html: ערכות שינויים מאת %{user_link}
-      title_followed: ×¢×¨×\9b×\95ת ×©×\99× ×\95×\99×\99×\9d ×©×\9c ×\9eשת×\9eש×\99×\9d ×\91ר×\99שמת המעקב שלך
+      title_followed: ×¢×¨×\9b×\95ת ×©×\99× ×\95×\99×\99×\9d ×©×\9c ×\9eשת×\9eש×\99×\9d ×\91רש×\99מת המעקב שלך
       title_nearby: ערכות שינויים של משתמשים בסביבה
       empty: לא נמצאה אף ערכת שינויים.
       empty_area: אין ערכות שינויים באזור הזה.
@@ -579,6 +579,8 @@ he:
       no_more: לא נמצאו ערכות שינויים נוספות.
       no_more_area: אין עוד ערכות שינויים באזור הזה.
       no_more_user: אין ערכות שינויים נוספות מאת המשתמש הזה.
+      older_changesets: ערכות שינויים ישנות יותר
+      newer_changesets: ערכות שינויים חדשות יותר
       feed:
         title: ערכת שינויים %{id}
         title_comment: ערכת שינויים %{id} – %{comment}
index b997f8c86a1bb2fea8bea13399c3389856152273..f6bcf19fa8a52af1edd22775264bc6be494e1878 100644 (file)
@@ -264,6 +264,8 @@ mk:
         agreed_with_pd: Исто така изјавивте дека вашите уредувања ги сметате за јавна
           сопственост.
         link text: што е ова?
+        not_agreed_with_pd: Исто така, немате изјавено дека вашите уредувања ги сметате
+          за јавна сопственост.
         pd_link_text: изјави
       save changes button: Зачувај ги промените
       delete_account: Избриши сметка...
@@ -345,7 +347,16 @@ mk:
         terms_declined_url: https://wiki.openstreetmap.org/wiki/Contributor_Terms_Declined?uselang=mk
     pd_declarations:
       show:
+        title: Сметај ги моите придонеси за јавна сопственост
+        consider_pd: Сметајте ги моите придонеси за јавна сопственост.
+        consider_pd_why: Зошто придонесите да ми бидат во јавна сопственост?
         confirm: Потврди
+      create:
+        successfully_declared: Успешно изјавивте дека вашите уредувања ги сметате
+          за јавна сопственост.
+        already_declared: Веќе изјавивте дека вашите уредувања ги сметате за јавна
+          сопственост.
+        did_not_confirm: Не потврдивте дека вашите уредувања ги сметате за јавна сопственост.
   browse:
     deleted_ago_by_html: Избришано %{time_ago} од %{user}
     edited_ago_by_html: Изменето %{time_ago} од %{user}
@@ -414,6 +425,7 @@ mk:
     start_rjs:
       feature_warning: Вчитувам %{num_features} елементи, што може да ви го забави
         прелистувачот. Дали сте сигурни дека сакате да се прикажат овие податоци?
+      feature_error: 'Функциите не можеа да се вчитаат: %{message}'
       load_data: Вчитај ги податоците
       loading: Вчитувам...
     tag_details:
@@ -490,10 +502,18 @@ mk:
         sorry: За жал, на списокот на измени што го побаравте му требаше предолго
           за да се преземе.
   changesets:
+    changeset:
+      comments:
+        one: '%{count} коментар'
+        other: '%{count} коментари'
+      changes:
+        one: '%{count} промена'
+        other: '%{count} промени'
     index:
       title: Измени
       title_user: Измени на %{user}
       title_user_link_html: Промени од %{user_link}
+      title_followed: Измени од корисници што ги следите
       title_nearby: Измени од соседни корисници
       empty: Не пронајдов промени.
       empty_area: Нема промени на ова подрачје.
@@ -575,6 +595,7 @@ mk:
         за да гледате корисници во близина.'
       edit_your_profile: Уредете си го профилот
       followings: Корисници што ги следите
+      no followings: Сè уште не следите ниеден корисник.
       nearby users: Други соседни корисници
       no nearby users: Сè уште нема други корисници во вашата околина што признаваат
         дека работат на карти.
@@ -590,6 +611,7 @@ mk:
       use_map_link: На карта
     index:
       title: Дневници на корисници
+      title_followed: Дневници на следени корисници
       title_nearby: Дневници на соседните корисници
       user_title: Дневникот на %{user}
       in_language_title: Дневнички написи на %{language}
@@ -1568,7 +1590,9 @@ mk:
       title: Проблеми
       select_status: Одберете статус
       select_type: Одберете тип
+      select_last_managed_by: Одберете подследен управувач
       reported_user: Пријавен корисник
+      not_managed: Не се управува
       search: Пребарај
       search_guidance: 'Пребарајте проблеми:'
       states:
@@ -1582,6 +1606,8 @@ mk:
       status: Статус
       reports: Пријави
       last_updated: Последна поднова
+      last_managed: Последен управувач
+      reporting_users: Пријавување на корисници
       reports_count:
         one: '%{count} пријава'
         other: '%{count} пријави'
@@ -1619,12 +1645,26 @@ mk:
       reopened: Состојбата на проблемот е поставена како „Отворено“
     comments:
       comment_from_html: Коментар од %{user_link} на %{comment_created_at}
+      reassign_to_moderators: Преназначете го проблемот на модераторите
+      reassign_to_administrators: Преназначете го проблемот на администраторите
     reports:
       reported_by_html: Пријавено како %{category} од %{user} на %{updated_at}
     helper:
       reportable_title:
         diary_comment: '%{entry_title}, коментар бр. %{comment_id}'
         note: Напомена бр. %{note_id}
+      reportable_heading:
+        diary_comment_html: Дневнички коментар %{title} создаден на %{datetime_created},
+          подновен на %{datetime_updated}
+        diary_entry_html: Дневничка ставка %{title} создадена на %{datetime_created},
+          подновена на %{datetime_updated}
+        note_html: '%{title} создадено на %{datetime_created}, подновено на %{datetime_updated}'
+        user_html: Корисник %{title} создаден на %{datetime_created}
+    reporters:
+      index:
+        title: Пријавувачи на проблем бр. %{issue_id}
+      reporters:
+        more_reporters: и уште %{count}
   issue_comments:
     create:
       comment_created: Коментарот е успешно создаден
@@ -1694,6 +1734,13 @@ mk:
     communities: Заедници
     learn_more: Дознајте повеќе
     more: Повеќе
+    offline_flash:
+      osm_offline: Базата на податоци на OpenStreetMap моментално е исклучена додека
+        работиме на неопходни одржувања.
+      osm_read_only: Базата на податоци на OpenStreetMap моментално може само да се
+        чита, додека ги извршиме неопходните одржувања.
+      expected_restore_html: Се очекува услугата да се поврати за %{time}.
+      announcement: Тука можете да го прочитате соопштението.
   user_mailer:
     diary_comment_notification:
       description: Дневничка ставка бр. %{id} во OpenStreetMap
@@ -1722,22 +1769,37 @@ mk:
         порака на авторот на %{replyurl}
     follow_notification:
       hi: Здраво %{to_user},
+      subject: '[OpenStreetMap] %{user} ве следи'
+      followed_you: '%{user} не ве следи на OpenStreetMap.'
       see_their_profile: Можете да го погледате профилот на оваа личност на %{userurl}.
       see_their_profile_html: Можете да го погледате профилот на оваа личност на %{userurl}.
+      follow_them: Можете да го следите лицето и на %{followurl}.
+      follow_them_html: Можете да го следите лицето и на %{followurl}.
+    gpx_details:
+      details: 'Поединости за вашата податотека:'
+      filename: Име на податотеката
+      url: URL
+      description: Опис
+      tags: Ознаки
+      total_points: Вкупно точки
+      imported_points: Број на увезени точки
     gpx_failure:
       hi: Здраво %{to_user},
-      failed_to_import: 'не можеше да се увезе. Проверете дали е важечка GPX-податотека
-        или архив што содржи GPX-податотека/ки во поддржан формат (.tar.gz, .tar.bz2,
-        .tar, .zip, .gpx.gz, .gpx.bz2). Да не има форматен или синтаксен проблем со
-        податотеката? Еве ја гршеката при увоз:'
-      more_info: Повеќе информации за неуспесите на увозот на GPX и тоа како да ги
-        одбегнете ќе најдете на %{url}.
+      failed_to_import: Изгледа дека вашата податотека не успеа да се увезе како ГПС-трага.
+      verify: 'Проверете дали е важечка GPX-податотека или архив што содржи GPX-податотека/ки
+        во поддржан формат (.tar.gz, .tar.bz2, .tar, .zip, .gpx.gz, .gpx.bz2). Да
+        не има форматен или синтаксен проблем со податотеката? Еве ја гршеката при
+        увоз:'
+      more_info: Повеќе информации за неуспесите на увозот на GPX и за тоа како да
+        ги одбегнете ќе најдете на %{url}.
       more_info_html: Повеќе информации за неуспесите на увозот на GPX и тоа како
         да ги одбегнете ќе најдете на %{url}.
       import_failures_url: https://wiki.openstreetmap.org/wiki/GPX_Import_Failures?uselang=mk
       subject: '[OpenStreetMap] Проблем при увозот на GPX податотека'
     gpx_success:
       hi: Здраво %{to_user},
+      imported_successfully: Изгледа дека вашата податотека е успешно увезена како
+        ГПС-трага.
       all_your_traces: Сите ваши успешно подигнати GPX-траги ќе ги најдете на %{url}
       all_your_traces_html: Сите ваши успешно подигнати GPX-траги ќе ги најдете на
         %{url}.
@@ -2045,6 +2107,9 @@ mk:
       preview: Преглед
       help: Помош
     pagination:
+      changeset_comments:
+        older: Постари коментари
+        newer: Понови коментари
       diary_comments:
         older: Постари коментари
         newer: Понови коментари
@@ -2743,7 +2808,7 @@ mk:
       openid: Најава со OpenStreetMap
       read_prefs: Кориснички нагодувања за читање
       write_prefs: Менување на корисничките нагодувања
-      write_diary: Создавање на дневнички ставки, коментирање и спријателување
+      write_diary: Создавање на дневнички ставки и коментири
       write_api: Менување на картата
       write_changeset_comments: Коментирање на измени
       read_gpx: Читање на приватни ГПС-траги
@@ -3189,7 +3254,8 @@ mk:
         другите картографи за да ја средиме работата. Поместете го бележникот на исправното
         место и внесете порака, објаснувајќи го проблемот.
       anonymous_warning_html: Не сте најавени. %{log_in} или %{sign_up} ако сакате
-        да ве известуваме за вашата белешка.
+        да ве известуваме за вашата белешка и да им помогнете на картографите да ја
+        решат.
       anonymous_warning_log_in: Најавете се
       anonymous_warning_sign_up: зачленете се
       counter_warning_html: Веќе имате објавено барем %{x_anonymous_notes}. Тоа е
index 30d57e8387d8f83db3416596e7388a90ee990a97..a684005bce6056c286261e20321feeacede459f4 100644 (file)
@@ -256,6 +256,7 @@ ne:
         heading: सर्तहरू
         heading_ct: योगदानकर्ता सर्तहरू
         continue: जारी राख्ने
+        cancel: रद्द गर्नुहोस्
         legale_names:
           france: फ्रान्स
           italy: इटाली
index c60e9604395f66bc1ab2759162bdbe6d9411fe05..21ed9a9b034ff538f9a1174b60fb46de7a5e3a00 100644 (file)
@@ -582,7 +582,7 @@ nl:
       title: Wijzigingensets
       title_user: Wijzigingensets door %{user}
       title_user_link_html: Wijzigingensets door %{user_link}
-      title_followed: Wijzigingensets door gevolgde kaartmakers
+      title_followed: Wijzigingensets door gebruikers die u volgt
       title_nearby: Wijzigingensets van gebruikers in de buurt
       empty: Geen wijzigingensets gevonden.
       empty_area: Geen wijzigingensets in dit gebied.
@@ -657,7 +657,7 @@ nl:
       no_home_location_html: '%{edit_profile_link} en stel uw thuislocatie in om gebruikers
         in de buurt te zien.'
       edit_your_profile: Pas uw profiel aan
-      followings: Gevolgde kaartmakers
+      followings: Gebruikers die u volgt
       no followings: Je volgt nog geen andere kaartmakers.
       nearby users: Andere nabije gebruikers
       no nearby users: Er zijn nog geen andere gebruikers die hebben opgegeven in
@@ -674,7 +674,7 @@ nl:
       use_map_link: Kaart gebruiken
     index:
       title: Gebruikersdagboeken
-      title_followed: Dagboeken van gevolgde kaartmakers
+      title_followed: Dagboeken van gevolgde gebruikers
       title_nearby: Dagboeken van gebruikers in de buurt
       user_title: Dagboek van %{user}
       in_language_title: Dagboekberichten in het %{language}
@@ -2280,8 +2280,7 @@ nl:
           van de kaart.
         credit_4_1_this_copyright_page: deze auteursrechtpagina
         attribution_example:
-          alt: Voorbeeld van hoe de naamsvermelding voor OpenStreetMap toe te passen
-            op een webpagina
+          alt: Voorbeeld van een naamsvermelding van OpenStreetMap op een webpagina
           title: Voorbeeld naamsvermelding
         more_title_html: Meer informatie
         more_1_1_html: Lees meer over het gebruik van onze gegevens en hoe u ons kunt
index bc3b9c6409450711fd99541f6c7eecd98c94c926..53279ebe411b13d2dbcb53049118a775165302e3 100644 (file)
@@ -49,6 +49,7 @@
 # Author: Naoliv
 # Author: Nemo bis
 # Author: Nighto
+# Author: Ninawauwau
 # Author: Pedrofariasm
 # Author: Re demz
 # Author: Rodrigo Avila
@@ -575,7 +576,7 @@ pt:
       title: Conjuntos de alterações
       title_user: Conjuntos de alterações de %{user}
       title_user_link_html: Conjuntos de alterações de %{user_link}
-      title_followed: Conjuntos de alterações das pessoas que sigo
+      title_followed: Conjuntos de alterações das pessoas seguidas
       title_nearby: Conjuntos de alterações de usuários próximos
       empty: Nenhum conjunto de alterações encontrado.
       empty_area: Nenhum conjunto de alterações nesta área.
@@ -612,6 +613,7 @@ pt:
       osmchangexml: XML osmChange
     paging_nav:
       nodes_paginated: Pontos (%{x}-%{y} de %{count})
+      ways_title: Rotas
       ways_paginated: Linhas (%{x}-%{y} de %{count})
       relations_paginated: Relações (%{x}-%{y} de %{count})
     not_found_message:
@@ -651,7 +653,7 @@ pt:
       no_home_location_html: '%{edit_profile_link} e defina seu local de origem para
         ver usuários próximos.'
       edit_your_profile: Editar seu perfil
-      followings: Pessoas que sigo
+      followings: Pessoas que você segue
       no followings: Você ainda não seguiu nenhum usuário.
       nearby users: Outros usuários próximos
       no nearby users: Ainda não há outros usuários mapeando por perto.
@@ -667,7 +669,7 @@ pt:
       use_map_link: Usar mapa
     index:
       title: Diários dos usuários
-      title_followed: Diários das pessoas que sigo
+      title_followed: Diários das pessoas seguidas
       title_nearby: Diários dos usuários próximos
       user_title: Diário de %{user}
       in_language_title: Publicações no diário em %{language}
@@ -1579,6 +1581,7 @@ pt:
           fishpond: Tanque de peixes
           lagoon: Lagoa
           wastewater: Agua residual
+          oxbow: Braço morto
           lock: Eclusa
         waterway:
           artificial: Via Aquática Artificial
@@ -1600,7 +1603,7 @@ pt:
           weir: Vertedouro
           "yes": Via Aquática
       admin_levels:
-        level2: Fronteira nacional
+        level2: Fronteira Nacional
         level3: Limite de região
         level4: Divisa Estadual
         level5: Limite Regional
@@ -1610,6 +1613,15 @@ pt:
         level9: Limite de Distrito Municipal
         level10: Limite de Bairro
         level11: Limite da vizinhança
+      border_types:
+        city: Limite de município
+        comarca: Limite de comarca
+        county: Limite de província
+        departement: Limite de departamento
+        department: Limite de departamento
+        district: Limite de distrito
+        distrito: Limite de distrito
+        freguesia: Limite de freguesia
     results:
       no_results: Nenhum resultado encontrado
       more_results: Mais resultados
@@ -2478,6 +2490,10 @@ pt:
       where_am_i_title: Descrever a localidade atual usando o motor de busca
       submit_text: Ir
       reverse_directions_text: Sentido contrário
+      modes:
+        bicycle: Bicicleta
+        car: Carro
+        foot: A pé
     welcome:
       title: Bem-vindo(a)!
       introduction: Bem-vindo(a) ao OpenStreetMap, o mapa livre e editável do mundo.
@@ -3444,6 +3460,7 @@ pt:
           ninth: 9.ª
           tenth: 10.ª
       time: Duração
+      filename: rota
     query:
       node: Ponto
       way: Linha
@@ -3459,8 +3476,8 @@ pt:
       query_features: Consultar elementos
       centre_map: Centralizar o mapa aqui
     home:
-      marker_title: A localização da minha casa
-      not_set: A localização da casa não está definido para sua conta
+      marker_title: Localização da minha casa
+      not_set: Localização da casa não está definida para sua conta
   redactions:
     edit:
       heading: Editar anulação
index 00e17ea6c90438d357154b9363c242bf8e1a89ea..3f8bbd2ac8ba74c19914bab90558ff98cd116c49 100644 (file)
@@ -607,7 +607,7 @@ uk:
       title: Набори змін
       title_user: Набори змін від %{user}
       title_user_link_html: Набори змін від %{user_link}
-      title_followed: Набори змін від тих за ким слідкуєш
+      title_followed: Набори змін від тих за ким стежиш
       title_nearby: Набори змін маперів поряд з вами
       empty: Жодного набору змін не знайдено.
       empty_area: На цій ділянці набори змін відсутні.
@@ -615,6 +615,8 @@ uk:
       no_more: Наборів змін більше не знайдено.
       no_more_area: Наборів змін на цій ділянці більше немає.
       no_more_user: Наборів змін від цього мапера більше немає.
+      older_changesets: Старіші набори змін
+      newer_changesets: Новіші набори змін
       feed:
         title: Набір змін %{id}
         title_comment: Набір змін %{id} — %{comment}
@@ -686,7 +688,7 @@ uk:
       no_home_location_html: '%{edit_profile_link} і встановіть своє місце розташування,
         щоб бачити маперів поруч.'
       edit_your_profile: Редагувати свій профіль
-      followings: Ð\92Ñ\96дÑ\81лÑ\96дковÑ\83Ñ\94Ñ\88
+      followings: Ð\9aоÑ\80иÑ\81Ñ\82Ñ\83ваÑ\87Ñ\96, Ð·Ð° Ñ\8fкими Ð²Ð¸ Ñ\81Ñ\82ежиÑ\82е
       no followings: Ви поки що не слідкуєте за жодним учасником.
       nearby users: Інші мапери поруч
       no nearby users: Поблизу поки немає маперів, які позначили своє розташування.
@@ -702,7 +704,7 @@ uk:
       use_map_link: Вказати на мапі
     index:
       title: Щоденники учасників
-      title_followed: Щоденники тих, за ким слідкуєш
+      title_followed: Щоденники тих, за ким стежиш
       title_nearby: Щоденники учасників поряд з вами
       user_title: Щоденник %{user}
       in_language_title: Записи щоденника мовою %{language}
@@ -1108,7 +1110,7 @@ uk:
           footway: Пішохідна доріжка
           ford: Брід
           give_way: Знак Дати путь
-          living_street: Ð\96иÑ\82лова Ð²Ñ\83лиÑ\86Ñ\8f
+          living_street: Ð\96иÑ\82лова Ð·Ð¾Ð½Ð°
           milestone: Кілометровий стовпчик
           motorway: Автомагістраль
           motorway_junction: В’їзд на автомагістраль
@@ -1183,7 +1185,7 @@ uk:
         information:
           guidepost: Вказівник напрямку
           board: Інформаційна дошка
-          map: Ð\9aаÑ\80Ñ\82а
+          map: Ð\9cапа
           office: Туристичний офіс
           terminal: Інформаційний термінал
           sign: Інформаційний знак
@@ -1479,7 +1481,7 @@ uk:
           car_repair: Автомайстерня
           carpet: Килими
           charity: Соціальний магазин
-          cheese: Ð¡Ð¸Ñ\80ний Ð¼Ð°Ð³Ð°Ð·Ð¸Ð½
+          cheese: Ð\9cагазин Ñ\81иÑ\80Ñ\83
           chemist: Побутова хімія
           chocolate: Шоколад
           clothes: Одяг
index e95ae2894982e6c756c61e5563aca08abcefea68..e041f6cf19a38294b0e0d269e096b221c1fde554 100644 (file)
@@ -255,14 +255,14 @@ zh-TW:
   api:
     notes:
       comment:
-        opened_at_html: 於%{when}建立
-        opened_at_by_html: 於%{when}由%{user}建立
+        opened_at_html: 於 %{when} 建立
+        opened_at_by_html: 於 %{when} 由 %{user} 建立
         commented_at_html: 於%{when}更新
-        commented_at_by_html: 於%{when}由%{user}更新
-        closed_at_html: 於%{when}已解決
-        closed_at_by_html: 於%{when}由%{user}關閉
-        reopened_at_html: 於%{when}重新開啟
-        reopened_at_by_html: 於%{when}由%{user}重新開啟
+        commented_at_by_html: 於 %{when} 由 %{user} 更新
+        closed_at_html: 於 %{when} 已解決
+        closed_at_by_html: 於 %{when} 由 %{user} 關閉
+        reopened_at_html: 於 %{when} 重新開啟
+        reopened_at_by_html: 於 %{when} 由 %{user} 重新開啟
       rss:
         title: 開放街圖註記
         description_all: 報告、評論、或關閉的註記清單
@@ -364,8 +364,8 @@ zh-TW:
         already_declared: 你已經宣稱你的貢獻屬於公共領域了。
         did_not_confirm: 你仍未宣稱你的貢獻屬於公共領域。
   browse:
-    deleted_ago_by_html: 由%{user}%{time_ago}刪除
-    edited_ago_by_html: 由%{user}%{time_ago}編輯
+    deleted_ago_by_html: 由 %{user} %{time_ago} 刪除
+    edited_ago_by_html: 由 %{user} %{time_ago} 編輯
     version: 版本
     redacted_version: 編修版本
     in_changeset: 變更集
@@ -493,7 +493,7 @@ zh-TW:
     feeds:
       comment:
         comment: '由 %{author} 對變更集 #%{changeset_id} 發表的新評論'
-        commented_at_by_html: 於%{when}由%{user}更新
+        commented_at_by_html: 於 %{when} 由 %{user} 更新
       show:
         title_all: 開放貼圖變更集討論
         title_particular: 開放街圖變更集 %{changeset_id} 討論
@@ -511,7 +511,7 @@ zh-TW:
       title: 變更集
       title_user: '%{user} 的變更集'
       title_user_link_html: '%{user_link} 的變更集'
-      title_followed: 追蹤者的變更集
+      title_followed: 你追蹤的使用者的變更集
       title_nearby: 附近使用者的變更集
       empty: 查無變更集。
       empty_area: 此區域沒有變更集。
@@ -519,6 +519,8 @@ zh-TW:
       no_more: 查無更多變更集。
       no_more_area: 此區域沒有更多變更集。
       no_more_user: 此使用者沒有更多變更集。
+      older_changesets: 較舊的變更集
+      newer_changesets: 較新的變更集
       feed:
         title: 變更集 %{id}
         title_comment: 變更集 %{id} - %{comment}
@@ -531,15 +533,15 @@ zh-TW:
       closed: 關閉於:%{when}
       created_ago_html: '%{time_ago}建立'
       closed_ago_html: '%{time_ago}關閉'
-      created_ago_by_html: 由%{user}%{time_ago}建立
-      closed_ago_by_html: 由%{user}%{time_ago}關閉
+      created_ago_by_html: 由 %{user} %{time_ago} 建立
+      closed_ago_by_html: 由 %{user} %{time_ago} 關閉
       discussion: 討論
       join_discussion: 登入以參加討論
       still_open: 變更集仍為開啟 - 討論要在變更集關閉後才會開啟。
       subscribe: 訂閱
       unsubscribe: 取消訂閱
-      comment_by_html: 來自%{user}%{time_ago}的評論
-      hidden_comment_by_html: '%{user}%{time_ago}隱藏評論'
+      comment_by_html: 來自 %{user} %{time_ago} 的評論
+      hidden_comment_by_html: '%{user} %{time_ago} 隱藏評論'
       hide_comment: 隱藏
       unhide_comment: 取消隱藏
       comment: 評論
@@ -575,7 +577,7 @@ zh-TW:
     contact:
       km away: '%{count} 公里遠'
       m away: '%{count} 公尺遠'
-      latest_edit_html: 上次編輯於%{ago}:
+      latest_edit_html: 上次編輯於 %{ago}:
       no_edits: (沒有編輯)
       view_changeset_details: 檢視變更集詳細資料
     popup:
@@ -586,7 +588,7 @@ zh-TW:
       title: 我的功能面板
       no_home_location_html: '%{edit_profile_link}並編輯你的家位置,來查看附近的使用者。'
       edit_your_profile: 編輯你的個人檔案
-      followings: 追蹤
+      followings: 你追蹤的使用
       no followings: 您尚未追蹤任何使用者。
       nearby users: 其他附近的使用者
       no nearby users: 附近沒有已加入製圖的使用者。
@@ -602,7 +604,7 @@ zh-TW:
       use_map_link: 使用地圖
     index:
       title: 使用者日記
-      title_followed: 追蹤者的日記
+      title_followed: 你追蹤的使用者的日記
       title_nearby: 附近的使用者的日記
       user_title: '%{user} 的日記'
       in_language_title: 語言為%{language}的日記項目
@@ -622,7 +624,7 @@ zh-TW:
       subscribe: 訂閱
       unsubscribe: 取消訂閱
       leave_a_comment: 留下評論
-      login_to_leave_a_comment_html: '%{login_link}來留下評論'
+      login_to_leave_a_comment_html: '%{login_link} 來留下評論'
       login: 登入
     no_such_entry:
       title: 沒有這樣的日記項目
@@ -708,7 +710,7 @@ zh-TW:
       title: 錯誤請求
       description: 你在開放街圖伺服器上請求的操作無效 (HTTP 400)
     forbidden:
-      title: Forbidden
+      title: 禁止
       description: 你在開放街圖伺服器上請求的運作僅限管理員使用 (HTTP 403}
     internal_server_error:
       title: 應用程式錯誤
@@ -1006,23 +1008,23 @@ zh-TW:
           path: 小徑
           pedestrian: 人行道
           platform: 月台
-          primary: 省道
-          primary_link: 省道聯絡道
+          primary: 一級道路
+          primary_link: 一級道路聯絡道
           proposed: 計畫中道路
           raceway: 賽道
           residential: 住宅區道路
           rest_area: 休息區
           road: 道路
-          secondary: 縣道
-          secondary_link: 縣道聯絡道
-          service: 專用道路
+          secondary: 二級道路
+          secondary_link: 次要道路聯絡道
+          service: 服務道路
           services: 高速公路服務區
           speed_camera: 測速相機
           steps: 階梯
           stop: 停止標誌
           street_lamp: 路燈
-          tertiary: 鄉道
-          tertiary_link: 鄉道聯絡道
+          tertiary: 三級道路
+          tertiary_link: 三級道路聯絡道
           track: 產業道路
           traffic_mirror: 道路反射鏡
           traffic_signals: 交通號誌
@@ -2377,8 +2379,8 @@ zh-TW:
         motorway: 高速公路
         main_road: 主要幹道
         trunk: 快速公路
-        primary: 省道
-        secondary: 縣道
+        primary: 一級道路
+        secondary: 二級道路
         unclassified: 未分級道路
         pedestrian: 人行道
         track: 產業道路
@@ -2517,8 +2519,8 @@ zh-TW:
       identifiable: 可辨識
       private: 私人
       trackable: 可追蹤
-      details_with_tags_html: 由%{user}在%{tags}於%{time_ago}
-      details_without_tags_html: 由%{user}於%{time_ago}
+      details_with_tags_html: 由 %{user} 在 %{tags} 於 %{time_ago}
+      details_without_tags_html: 由 %{user} 於 %{time_ago}
     index:
       public_traces: 公開 GPS 軌跡
       my_gps_traces: 我的 GPS 軌跡
@@ -2831,7 +2833,7 @@ zh-TW:
         comment: 評論
     diary_comments:
       index:
-        title: 日記評論由%{user}新增
+        title: 日記評論由 %{user} 新增
       page:
         post: 貼文
         when: 於
@@ -2944,9 +2946,9 @@ zh-TW:
     navigation:
       all_blocks: 所有封鎖
       blocks_on_me: 對我的封鎖
-      blocks_on_user_html: 對%{user}的封鎖
+      blocks_on_user_html: 對 %{user} 的封鎖
       blocks_by_me: 由我做出的封鎖
-      blocks_by_user_html: 由%{user}做出的封鎖
+      blocks_by_user_html: 由 %{user} 做出的封鎖
       block: 封鎖#%{id}
       new_block: 新封鎖
   user_mutes:
@@ -2975,7 +2977,7 @@ zh-TW:
     index:
       title: 由 %{user} 送出或評論的註記
       heading: '%{user} 的註記'
-      subheading_html: 由%{user}%{submitted}或%{commented}的註記
+      subheading_html: 由 %{user} %{submitted} 或 %{commented} 的註記
       subheading_submitted: 已提交
       subheading_commented: 已評論
       no_notes: 沒有註記
@@ -2997,15 +2999,15 @@ zh-TW:
       hidden_title: 已隱藏的註記:#%{note_name}
       description_when_author_is_deleted: 已刪除
       description_when_there_is_no_opening_comment: 未知的
-      event_opened_by_html: 由%{user}%{time_ago}建立
+      event_opened_by_html: 由 %{user} %{time_ago} 建立
       event_opened_by_anonymous_html: 由匿名使用者%{time_ago}建立
-      event_commented_by_html: 來自%{user}%{time_ago}的評論
+      event_commented_by_html: 來自 %{user} %{time_ago} 的評論
       event_commented_by_anonymous_html: 來自匿名使用者%{time_ago}的評論
-      event_closed_by_html: 由%{user}%{time_ago}解決
+      event_closed_by_html: 由 %{user} %{time_ago} 解決
       event_closed_by_anonymous_html: 由匿名使用者%{time_ago}解決
-      event_reopened_by_html: 由%{user} %{time_ago}重新開啟
+      event_reopened_by_html: 由 %{user} %{time_ago} 重新開啟
       event_reopened_by_anonymous_html: 由匿名使用者%{time_ago}重新開啟
-      event_hidden_by_html: 由%{user} %{time_ago}隱藏
+      event_hidden_by_html: 由 %{user} %{time_ago} 隱藏
       report: 回報此註記
       anonymous_warning: 此項註記包含來自匿名使用者的評論,應作獨立核實。
       discussion: 討論
index 4b72e432e50721a5e947a10860fdd7ca440d5876..c20b9bddcf169631b6052a1d4baf2eb098af3f7d 100644 (file)
@@ -53,8 +53,8 @@ module Api
       user = create(:user)
       changeset = create(:changeset, :user => user)
       closed_changeset = create(:changeset, :closed, :user => user, :created_at => Time.utc(2008, 1, 1, 0, 0, 0), :closed_at => Time.utc(2008, 1, 2, 0, 0, 0))
-      changeset2 = create(:changeset, :min_lat => (5 * GeoRecord::SCALE).round, :min_lon => (5 * GeoRecord::SCALE).round, :max_lat => (15 * GeoRecord::SCALE).round, :max_lon => (15 * GeoRecord::SCALE).round)
-      changeset3 = create(:changeset, :min_lat => (4.5 * GeoRecord::SCALE).round, :min_lon => (4.5 * GeoRecord::SCALE).round, :max_lat => (5 * GeoRecord::SCALE).round, :max_lon => (5 * GeoRecord::SCALE).round)
+      changeset2 = create(:changeset, :bbox => [5, 5, 15, 15])
+      changeset3 = create(:changeset, :bbox => [4.5, 4.5, 5, 5])
 
       get api_changesets_path(:bbox => "-10,-10, 10, 10")
       assert_response :success, "can't get changesets in bbox"
@@ -627,9 +627,7 @@ module Api
     end
 
     def test_show_bbox_json
-      # test bbox attribute
-      changeset = create(:changeset, :min_lat => (-5 * GeoRecord::SCALE).round, :min_lon => (5 * GeoRecord::SCALE).round,
-                                     :max_lat => (15 * GeoRecord::SCALE).round, :max_lon => (12 * GeoRecord::SCALE).round)
+      changeset = create(:changeset, :bbox => [5, -5, 12, 15])
 
       get api_changeset_path(changeset, :format => "json")
       assert_response :success, "cannot get first changeset"
@@ -2092,9 +2090,7 @@ module Api
       create(:changeset, :user => user, :created_at => Time.now.utc - 7.days)
 
       # create a changeset that puts us near the initial size limit
-      changeset = create(:changeset, :user => user,
-                                     :min_lat => (-0.5 * GeoRecord::SCALE).round, :min_lon => (0.5 * GeoRecord::SCALE).round,
-                                     :max_lat => (0.5 * GeoRecord::SCALE).round, :max_lon => (2.5 * GeoRecord::SCALE).round)
+      changeset = create(:changeset, :user => user, :bbox => [0.5, -0.5, 2.5, 0.5])
 
       # create authentication header
       auth_header = bearer_authorization_header user
index 0f9c4b8ce72cb88b2bf3792b57eb9c17bccd9eb9..007a5d679e35f726d5077ea3afcd9d3c93b356e8 100644 (file)
@@ -91,8 +91,8 @@ class ChangesetsControllerTest < ActionDispatch::IntegrationTest
   ##
   # This should display the last 20 changesets closed in a specific area
   def test_index_bbox
-    changesets = create_list(:changeset, 10, :num_changes => 1, :min_lat => 50000000, :max_lat => 50000001, :min_lon => 50000000, :max_lon => 50000001)
-    other_changesets = create_list(:changeset, 10, :num_changes => 1, :min_lat => 0, :max_lat => 1, :min_lon => 0, :max_lon => 1)
+    changesets = create_list(:changeset, 10, :num_changes => 1, :bbox => [5, 5, 5, 5])
+    other_changesets = create_list(:changeset, 10, :num_changes => 1, :bbox => [0, 0, 1, 1])
 
     # First check they all show up without a bbox parameter
     get history_path(:format => "html", :list => "1"), :xhr => true
@@ -117,6 +117,127 @@ class ChangesetsControllerTest < ActionDispatch::IntegrationTest
     check_index_result(changesets)
   end
 
+  def test_index_bbox_across_antimeridian_with_changesets_close_to_antimeridian
+    west_of_antimeridian_changeset = create(:changeset, :num_changes => 1, :bbox => [176, 0, 178, 1])
+    east_of_antimeridian_changeset = create(:changeset, :num_changes => 1, :bbox => [-178, 0, -176, 1])
+
+    get history_path(:format => "html", :list => "1")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset, west_of_antimeridian_changeset])
+
+    # negative longitudes
+    get history_path(:format => "html", :list => "1", :bbox => "-190,-10,-170,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset, west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-183,-10,-177,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset, west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-181,-10,-177,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-183,-10,-179,10")
+    assert_response :success
+    check_index_result([west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-181,-10,-179,10")
+    assert_response :success
+    check_index_result([])
+
+    # positive longitudes
+    get history_path(:format => "html", :list => "1", :bbox => "170,-10,190,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset, west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "177,-10,183,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset, west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "177,-10,181,10")
+    assert_response :success
+    check_index_result([west_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "179,-10,183,10")
+    assert_response :success
+    check_index_result([east_of_antimeridian_changeset])
+
+    get history_path(:format => "html", :list => "1", :bbox => "179,-10,181,10")
+    assert_response :success
+    check_index_result([])
+  end
+
+  def test_index_bbox_across_antimeridian_with_changesets_around_globe
+    changeset1 = create(:changeset, :num_changes => 1, :bbox => [-150, 40, -140, 50])
+    changeset2 = create(:changeset, :num_changes => 1, :bbox => [-30, -30, -20, -20])
+    changeset3 = create(:changeset, :num_changes => 1, :bbox => [10, 60, 20, 70])
+    changeset4 = create(:changeset, :num_changes => 1, :bbox => [150, -60, 160, -50])
+
+    # no bbox, get all changesets
+    get history_path(:format => "html", :list => "1")
+    assert_response :success
+    check_index_result([changeset4, changeset3, changeset2, changeset1])
+
+    # large enough bbox within normal range
+    get history_path(:format => "html", :list => "1", :bbox => "-170,-80,170,80")
+    assert_response :success
+    check_index_result([changeset4, changeset3, changeset2, changeset1])
+
+    # bbox for [1,2] within normal range
+    get history_path(:format => "html", :list => "1", :bbox => "-160,-80,0,80")
+    assert_response :success
+    check_index_result([changeset2, changeset1])
+
+    # bbox for [1,4] containing antimeridian with negative lon
+    get history_path(:format => "html", :list => "1", :bbox => "-220,-80,-100,80")
+    assert_response :success
+    check_index_result([changeset4, changeset1])
+
+    # bbox for [1,4] containing antimeridian with positive lon
+    get history_path(:format => "html", :list => "1", :bbox => "100,-80,220,80")
+    assert_response :success
+    check_index_result([changeset4, changeset1])
+
+    # large enough bbox outside normal range
+    get history_path(:format => "html", :list => "1", :bbox => "-220,-80,220,80")
+    assert_response :success
+    check_index_result([changeset4, changeset3, changeset2, changeset1])
+  end
+
+  ##
+  # Test that -180..180 longitudes don't result in empty bbox
+  def test_index_bbox_entire_world
+    changeset = create(:changeset, :num_changes => 1, :bbox => [30, 60, 31, 61])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-180,-80,-180,80")
+    assert_response :success
+    check_index_result([])
+
+    get history_path(:format => "html", :list => "1", :bbox => "180,-80,180,80")
+    assert_response :success
+    check_index_result([])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-180,-80,180,80")
+    assert_response :success
+    check_index_result([changeset])
+  end
+
+  ##
+  # Test that -270..270 longitudes don't result in 90..-90 bbox
+  def test_index_bbox_larger_than_entire_world
+    changeset1 = create(:changeset, :num_changes => 1, :bbox => [30, 60, 31, 61])
+    changeset2 = create(:changeset, :num_changes => 1, :bbox => [130, 60, 131, 61])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-90,-80,90,80")
+    assert_response :success
+    check_index_result([changeset1])
+
+    get history_path(:format => "html", :list => "1", :bbox => "-270,-80,270,80")
+    assert_response :success
+    check_index_result([changeset2, changeset1])
+  end
+
   ##
   # Checks the display of the user changesets listing
   def test_index_user
@@ -389,12 +510,12 @@ class ChangesetsControllerTest < ActionDispatch::IntegrationTest
   ##
   # This should display the last 20 changesets closed in a specific area
   def test_feed_bbox
-    changeset = create(:changeset, :num_changes => 1, :min_lat => 5 * GeoRecord::SCALE, :min_lon => 5 * GeoRecord::SCALE, :max_lat => 5 * GeoRecord::SCALE, :max_lon => 5 * GeoRecord::SCALE)
+    changeset = create(:changeset, :num_changes => 1, :bbox => [5, 5, 5, 5])
     create(:changeset_tag, :changeset => changeset)
     create(:changeset_tag, :changeset => changeset, :k => "website", :v => "http://example.com/")
-    closed_changeset = create(:changeset, :closed, :num_changes => 1, :min_lat => 5 * GeoRecord::SCALE, :min_lon => 5 * GeoRecord::SCALE, :max_lat => 5 * GeoRecord::SCALE, :max_lon => 5 * GeoRecord::SCALE)
-    _elsewhere_changeset = create(:changeset, :num_changes => 1, :min_lat => -5 * GeoRecord::SCALE, :min_lon => -5 * GeoRecord::SCALE, :max_lat => -5 * GeoRecord::SCALE, :max_lon => -5 * GeoRecord::SCALE)
-    _empty_changeset = create(:changeset, :num_changes => 0, :min_lat => -5 * GeoRecord::SCALE, :min_lon => -5 * GeoRecord::SCALE, :max_lat => -5 * GeoRecord::SCALE, :max_lon => -5 * GeoRecord::SCALE)
+    closed_changeset = create(:changeset, :closed, :num_changes => 1, :bbox => [5, 5, 5, 5])
+    _elsewhere_changeset = create(:changeset, :num_changes => 1, :bbox => [-5, -5, -5, -5])
+    _empty_changeset = create(:changeset, :num_changes => 0, :bbox => [5, 5, 5, 5])
 
     get history_feed_path(:format => :atom, :bbox => "4.5,4.5,5.5,5.5")
     assert_response :success
index d4ebdcee133d8811822db1f8974e8f13066f3b27..6a89a7d37031b9dc035d155aa21869f176f05f20 100644 (file)
@@ -1,7 +1,15 @@
 FactoryBot.define do
   factory :changeset do
+    transient do
+      bbox { nil }
+    end
+
     created_at { Time.now.utc }
     closed_at { Time.now.utc + 1.day }
+    min_lon { (bbox[0] * GeoRecord::SCALE).round if bbox }
+    min_lat { (bbox[1] * GeoRecord::SCALE).round if bbox }
+    max_lon { (bbox[2] * GeoRecord::SCALE).round if bbox }
+    max_lat { (bbox[3] * GeoRecord::SCALE).round if bbox }
 
     user
 
index 6c81a928fad768f4f5ac1c8f97208bc4c8886c78..c690551c74b576814cbe41cf3065000d4c2fea6c 100644 (file)
@@ -2,8 +2,7 @@ require "application_system_test_case"
 
 class BrowseCommentLinksTest < ApplicationSystemTestCase
   test "visiting changeset comment link should pan to changeset" do
-    changeset = create(:changeset, :min_lat => 60 * GeoRecord::SCALE, :min_lon => 30 * GeoRecord::SCALE,
-                                   :max_lat => 60 * GeoRecord::SCALE, :max_lon => 30 * GeoRecord::SCALE)
+    changeset = create(:changeset, :bbox => [30, 60, 30, 60])
     comment = create(:changeset_comment, :changeset => changeset, :body => "Linked changeset comment")
 
     visit changeset_path(changeset, :anchor => "c#{comment.id}")
index 5c66144c1dbe59b38dc27e8f510877637198d68e..cb36473541757a33a86bd5c0898ac1da6a4d1d0d 100644 (file)
@@ -110,9 +110,9 @@ class HistoryTest < ApplicationSystemTestCase
   end
 
   test "update sidebar when before param is included and map is moved" do
-    changeset1 = create(:changeset, :num_changes => 1, :min_lat => 50000000, :max_lat => 50000001, :min_lon => 50000000, :max_lon => 50000001)
+    changeset1 = create(:changeset, :num_changes => 1, :bbox => [5, 5, 5, 5])
     create(:changeset_tag, :changeset => changeset1, :k => "comment", :v => "changeset-at-fives")
-    changeset2 = create(:changeset, :num_changes => 1, :min_lat => 50100000, :max_lat => 50100001, :min_lon => 50100000, :max_lon => 50100001)
+    changeset2 = create(:changeset, :num_changes => 1, :bbox => [5.01, 5.01, 5.01, 5.01])
     create(:changeset_tag, :changeset => changeset2, :k => "comment", :v => "changeset-close-to-fives")
     changeset3 = create(:changeset)
 
@@ -136,9 +136,7 @@ class HistoryTest < ApplicationSystemTestCase
   test "all changesets are listed when fully zoomed out" do
     user = create(:user)
     [-177, -90, 0, 90, 177].each do |lon|
-      create(:changeset, :user => user, :num_changes => 1,
-                         :min_lat => 0 * GeoRecord::SCALE, :min_lon => (lon - 1) * GeoRecord::SCALE,
-                         :max_lat => 1 * GeoRecord::SCALE, :max_lon => (lon + 1) * GeoRecord::SCALE) do |changeset|
+      create(:changeset, :user => user, :num_changes => 1, :bbox => [lon - 1, 0, lon + 1, 1]) do |changeset|
         create(:changeset_tag, :changeset => changeset, :k => "comment", :v => "changeset-at-lon(#{lon})")
       end
     end
@@ -154,6 +152,30 @@ class HistoryTest < ApplicationSystemTestCase
     end
   end
 
+  test "changesets at both sides of antimeridian are listed" do
+    user = create(:user)
+    PAGE_SIZE.times do
+      create(:changeset, :user => user, :num_changes => 1, :bbox => [176, 0, 178, 1]) do |changeset|
+        create(:changeset_tag, :changeset => changeset, :k => "comment", :v => "West-of-antimeridian-changeset")
+      end
+      create(:changeset, :user => user, :num_changes => 1, :bbox => [-178, 0, -176, 1]) do |changeset|
+        create(:changeset_tag, :changeset => changeset, :k => "comment", :v => "East-of-antimeridian-changeset")
+      end
+    end
+
+    visit history_path(:anchor => "map=6/0/179")
+
+    within_sidebar do
+      assert_link "West-of-antimeridian-changeset", :count => PAGE_SIZE / 2
+      assert_link "East-of-antimeridian-changeset", :count => PAGE_SIZE / 2
+
+      click_on "Older Changesets"
+
+      assert_link "West-of-antimeridian-changeset", :count => PAGE_SIZE
+      assert_link "East-of-antimeridian-changeset", :count => PAGE_SIZE
+    end
+  end
+
   private
 
   def create_visible_changeset(user, comment)