Search code examples
javascripthtmlweb-applicationsleaflet

Adding overlay to LeafletJs map is changing data, and I have no idea why


So my team is working on a web application that uses LeafletJs and has a requirement for Importing and Exporting data produced in the application. This includes coverage data. We've ran into (what seems like) a bug were the data we are importing an exporting is being changed by an event in leaflet and we have no idea why.

I've produced a little something in a Codepen (first time using it so sorry if there are any issues) to isolate the problem as best as I can.

Codepen

Recreation Steps

Open the webapp and click on Export (and save depending on your browser's behavior. You should have a JSON file that looks like the object in the Codepen's JS file if you want to check it out before downloading something from a stranger. This file should work fine.

Now click on TEMP to add the overlay to the Leaflet map. This is the action that somehow is breaking the data. If you click export again you'll get a different file that doesn't work.

If you import the first file and then click on the newly added checkbox it will add the overlay just fine.

If you import the second file, it will import without an error but if you attempt to use the checkbox to add it as an overlay it will fail. IF you compare the two JSON files they are quite different. Though they should be exactly the same.

It's definitely the act of adding the data to the map that is making these changes, as removing it and exporting results in the same mutated JSON file rather than going back the original.

Source Code

HTML

<!DOCTYPE html>
<meta charset="UTF-8">
<html lang="en">
<head>
    <title>Demo</title>
    <link rel="stylesheet" type="text/css" href="index.css">
    <!-- Leaflet.core -->
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
          crossorigin="">
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"
            integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
            crossorigin=""></script>
    <!-- CoverageJSON Reader -->
    <script src="https://unpkg.com/covutils/covutils-lite.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/covjson-reader/0.16.3/covjson-reader.min.js"
            integrity="sha512-n8pZdTvMgAYZSgA0zOjItMZKeSEkQkxWJkVAAV3VMmmmGdEX5EHbqIYF0fuWYHPvKrz4X7fryuYcuR0ows9Ohw=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!-- Leaflet Coverage   -->
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.css">
    <link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/leaflet-coverage.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.js"></script>
    <script src="https://unpkg.com/[email protected]/covutils.min.js"></script>
    <script src="https://unpkg.com/[email protected]/leaflet-coverage.min.js"></script>

</head>

<body>
<a id="dataExport">
    <button id="exportButton" onclick="exportData()">Export</button>
</a>
<label id="importLabel" for="dataImport">Import</label>
<input id="dataImport" type="file" name="dataImport" onchange="importData()"/>
    <div class='map' id="map"></div>
</body>
<script src="index.js"></script>
</html>

JS

const baseLayer = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'});

const bounds = [[48, -12,], [61.5, 3]];

const map = L.map('map', {
    layers: [baseLayer],
    maxBounds: bounds,
    maxBoundsViscosity: 0.9,
}).setView([((bounds[1][0] + bounds[0][0]) / 2), ((bounds[1][1] + bounds[0][1]) / 2)], 5);

const controlLayers = L.control.layers({"OSM": baseLayer}, undefined, {collapsed: false}).addTo(map);

const dataArray = [];

loadCoverageData(mockData); // Mocking a fetch and response

function loadCoverageData(data) {
    console.log("Attempting to load: \n", data);
    dataArray.push(data) // User could have multiple layers of 
    CovJSON.read(data)
        .then((coverage) => {
            let dataLayer = C.dataLayer(coverage, {parameter: Object.keys(data.parameters)[0]})
                .on('afterAdd', () => {
                    if (dataLayer.palette) {
                        C.legend(dataLayer).addTo(map)
                    }
                    if (dataLayer.timeSlices) {
                        new C.TimeAxis(dataLayer).addTo(map)
                    }
                })

            controlLayers.addOverlay(dataLayer, Object.keys(data.parameters)[0]);
        });
}

function replacer(key, value) {
    if (value instanceof Map) {
        return {
            dataType: 'Map',
            value: Array.from(value.entries()),
        };
    } else {
        return value;
    }
}

function reviver(key, value) {
    if (typeof value === 'object' && value !== null) {
        if (value.dataType === 'Map') {
            return new Map(value.value);
        }
    }
    return value;
}

function exportData() {
    let encodedData = 'text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(dataArray, replacer));
    document.getElementById('dataExport').setAttribute('href', 'data:' + encodedData);
    document.getElementById('dataExport').setAttribute('download', 'drawing-data.json');
}

function importData() {
    let dataFile = document.getElementById('dataImport').files[0]
    let reader = new FileReader();
    reader.onload = function (event) {
        let data = JSON.parse(event.target.result, reviver);
        data.forEach((coverageSet) => {
            loadCoverageData(coverageSet);
        })
    }
    reader.readAsText(dataFile);
}

const mockData = {
  "type": "Coverage",
  "domain": {
    "type": "Domain",
    "domainType": "MultiPolygon",
    "axes": {
      "composite": {
        "dataType": "polygon",
        "coordinates": [
          "x",
          "y"
        ],
        "values": [
          [
            [
              [
                9.92,
                54.98
              ],
              [
                9.93,
                54.59
              ],
              [
                13.64,
                54.07
              ],
              [
                15.01,
                51.10
              ],
              [
                12.24,
                50.26
              ],
              [
                12.52,
                49.54
              ],
              [
                13.59,
                48.87
              ],
              [
                12.88,
                48.28
              ],
              [
                13.02,
                47.63
              ],
              [
                9.59,
                47.52
              ],
              [
                8.52,
                47.83
              ],
              [
                7.46,
                47.62
              ],
              [
                8.09,
                49.01
              ],
              [
                6.18,
                49.46
              ],
              [
                5.98,
                51.85
              ],
              [
                6.84,
                52.22
              ],
              [
                7.10,
                53.69
              ],
              [
                8.80,
                54.02
              ],
              [
                8.52,
                54.96
              ],
              [
                9.92,
                54.98
              ]
            ]
          ],
          [
            [
              [
                -3.00,
                58.63
              ],
              [
                -1.95,
                57.68
              ],
              [
                -2.08,
                55.90
              ],
              [
                0.46,
                52.92
              ],
              [
                1.68,
                52.73
              ],
              [
                0.55,
                50.76
              ],
              [
                -3.61,
                50.22
              ],
              [
                -5.26,
                51.99
              ],
              [
                -3.63,
                54.61
              ],
              [
                -5.58,
                55.31
              ],
              [
                -6.14,
                56.78
              ],
              [
                -5.00,
                58.63
              ],
              [
                -3.00,
                58.63
              ]
            ]
          ]
        ]
      },
      "t": {
        "values": [
          "2015"
        ]
      }
    },
    "referencing": [
      {
        "coordinates": [
          "x",
          "y"
        ],
        "system": {
          "type": "GeographicCRS",
          "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
        }
      },
      {
        "coordinates": [
          "t"
        ],
        "system": {
          "type": "TemporalRS",
          "calendar": "Gregorian"
        }
      }
    ]
  },
  "parameters": {
    "TEMP": {
      "type": "Parameter",
      "unit": {
        "label": {
          "en": "Degree Celsius"
        },
        "symbol": {
          "value": "Cel",
          "type": "http://www.opengis.net/def/uom/UCUM/"
        }
      },
      "observedProperty": {
        "label": {
          "en": "Average air temperature"
        }
      }
    }
  },
  "ranges": {
    "TEMP": {
      "type": "NdArray",
      "dataType": "float",
      "axisNames": [
        "composite"
      ],
      "shape": [
        2
      ],
      "values": [
        22.8,
        15.1
      ]
    }
  }
}

CS

.map {
 height: 80vh;
 width: 80vw;
}

Solution

  • Because of the use of data in CovJSON.read(data) in loadCoverageData the javascript reference does still exists and is overwritten / modifyied dynamically:

    function loadCoverageData(data) {
        console.log("Attempting to load: \n", data);
        dataArray.push(data) // User could have multiple layers of 
        CovJSON.read(data).then((coverage) => {
    

    Simple "copy" the data object and break the reference. One solution is to use data = JSON.parse(JSON.stringify(data)):

    function loadCoverageData(data) {
        console.log("Attempting to load: \n", data);
        dataArray.push(data) // User could have multiple layers of 
        data = JSON.parse(JSON.stringify(data))
        CovJSON.read(data).then((coverage) => {