1 //= require download_util
 
   2 OSM.initializeDataLayer = function (map) {
 
   3   let dataLoader, loadedBounds;
 
   4   const dataLayer = map.dataLayer;
 
   6   dataLayer.isWayArea = function () {
 
  10   dataLayer.on("click", function (e) {
 
  11     const feature = e.layer.feature;
 
  12     OSM.router.click(e.originalEvent, `/${feature.type}/${feature.id}`);
 
  15   dataLayer.on("add", function () {
 
  16     map.fire("overlayadd", { layer: this });
 
  17     map.on("moveend", updateData);
 
  21   dataLayer.on("remove", function () {
 
  22     if (dataLoader) dataLoader.abort();
 
  24     map.off("moveend", updateData);
 
  25     $("#browse_status").empty();
 
  26     map.fire("overlayremove", { layer: this });
 
  29   function updateData() {
 
  30     const bounds = map.getBounds();
 
  31     if (!loadedBounds || !loadedBounds.contains(bounds)) {
 
  36   function displayFeatureWarning(num_features, add, cancel) {
 
  37     $("#browse_status").html(
 
  38       $("<div class='p-3'>").append(
 
  39         $("<div class='d-flex'>").append(
 
  40           $("<h2 class='flex-grow-1 text-break'>")
 
  41             .text(OSM.i18n.t("browse.start_rjs.load_data")),
 
  43             $("<button type='button' class='btn-close'>")
 
  44               .attr("aria-label", OSM.i18n.t("javascripts.close"))
 
  46         $("<p class='alert alert-warning'>")
 
  47           .text(OSM.i18n.t("browse.start_rjs.feature_warning", { num_features })),
 
  48         $("<input type='submit' class='btn btn-primary d-block mx-auto'>")
 
  49           .val(OSM.i18n.t("browse.start_rjs.load_data"))
 
  55      * Modern browsers are quite happy showing far more than 100 features in
 
  56      * the data browser, so increase the limit to 4000.
 
  58     const maxFeatures = 4000;
 
  59     const bounds = map.getBounds();
 
  61     if (dataLoader) dataLoader.abort();
 
  63     $("#layers-data-loading").remove();
 
  65     const spanLoading = $("<span>")
 
  66       .attr("id", "layers-data-loading")
 
  67       .attr("class", "spinner-border spinner-border-sm ms-1")
 
  68       .attr("role", "status")
 
  69       .html("<span class='visually-hidden'>" + OSM.i18n.t("browse.start_rjs.loading") + "</span>")
 
  70       .appendTo($("#label-layers-data"));
 
  72     dataLoader = new AbortController();
 
  74     function getWrappedBounds(bounds) {
 
  75       const sw = bounds.getSouthWest().wrap();
 
  76       const ne = bounds.getNorthEast().wrap();
 
  85     function getRequestBounds(bounds) {
 
  86       const wrapped = getWrappedBounds(bounds);
 
  87       if (wrapped.minLng > wrapped.maxLng) {
 
  88         // BBox is crossing antimeridian: split into two bboxes in order to stay
 
  89         // within OSM API's map endpoint permitted range for longitude [-180..180].
 
  91           L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, 180]),
 
  92           L.latLngBounds([wrapped.minLat, -180], [wrapped.maxLat, wrapped.maxLng])
 
  95       return [L.latLngBounds([wrapped.minLat, wrapped.minLng], [wrapped.maxLat, wrapped.maxLng])];
 
  98     function fetchDataForBounds(bounds) {
 
  99       return fetch(`/api/${OSM.API_VERSION}/map.json?bbox=${bounds.toBBoxString()}`, {
 
 100         headers: { ...OSM.oauth },
 
 101         signal: dataLoader.signal
 
 105     const requestBounds = getRequestBounds(bounds);
 
 106     const requests = requestBounds.map(fetchDataForBounds);
 
 108     Promise.all(requests)
 
 111           responses.map(async response => {
 
 113               return response.json();
 
 116             const status = response.statusText || response.status;
 
 117             if (response.status !== 400 && response.status !== 509) {
 
 118               throw new Error(status);
 
 121             const text = await response.text();
 
 122             throw new Error(text || status);
 
 127         dataLayer.clearLayers();
 
 128         const allElements = dataArray.flatMap(item => item.elements);
 
 129         const originalFeatures = dataLayer.buildFeatures({ elements: allElements });
 
 130         // clone features when crossing antimeridian to work around Leaflet restrictions
 
 131         const features = requestBounds.length > 1 ?
 
 132           [...originalFeatures, ...cloneFeatures(originalFeatures)] : originalFeatures;
 
 134         function addFeatures() {
 
 135           $("#browse_status").empty();
 
 136           dataLayer.addData(features);
 
 137           loadedBounds = bounds;
 
 140         function cancelAddFeatures() {
 
 141           $("#browse_status").empty();
 
 144         if (features.length < maxFeatures * requestBounds.length) {
 
 147           displayFeatureWarning(features.length, addFeatures, cancelAddFeatures);
 
 150         if (map._objectLayer) {
 
 151           map._objectLayer.bringToFront();
 
 154       .catch(function (error) {
 
 155         if (error.name === "AbortError") return;
 
 157         OSM.displayLoadError(error?.message, () => {
 
 158           $("#browse_status").empty();
 
 163         spanLoading.remove();
 
 167   function cloneFeatures(features) {
 
 168     const offset = map.getCenter().lng < 0 ? -360 : 360;
 
 170     const cloneNode = ({ latLng, ...rest }) => ({
 
 172       latLng: { ...latLng, lng: latLng.lng + offset }
 
 175     return features.flatMap(feature => {
 
 176       if (feature.type === "node") {
 
 177         return [cloneNode(feature)];
 
 180       if (feature.type === "way") {
 
 181         const clonedNodes = feature.nodes.map(cloneNode);
 
 182         return [{ ...feature, nodes: clonedNodes }];