Search code examples
javascripthtmlleafletgeospatialleaflet.markercluster

Making Leaflet.FeatureGroup.SubGroup work so that user can upload CSV file and markers display


I created a code that uses a CSV file that is imported into the project folder so that it can display the Markers for each data point, while making Marker Clusters and Layers.

My code works great, but my boss would like me to add a function so that he could add a CSV file himself on the interface.

I figured out another code so that he can add a CSV file and display the Markers, but I don't know how to apply it to the code I already have so that the Layers and Marker SubGroups still work.

Here's my original code that works great and I want to keep, just with the added CSV file uploaded feature:

    var engineerLayers = {}; // Create an object to hold the engineer marker cluster groups

    //this retrieves the file
    $.get('\practice.csv', function (csvString) {
      var data = Papa.parse(csvString, {
        header: true,
        dynamicTyping: true
      }).data;
      var parentGroup = L.markerClusterGroup();

      for (var i in data) {
        var row = data[i];
        var engineer = row.Engineer;

        if (!(engineer in engineerLayers)) {
          // Each overlay that should actually be a part of MCG
          // should be made as a Subgroup, with the MCG as their parent
          engineerLayers[engineer] = L.featureGroup.subGroup(parentGroup, []);
          controlLayers.addOverlay(engineerLayers[engineer], engineer);
        }

        var marker = L.circleMarker([row.Latitude, row.Longitude], {
          radius: 10,
          stroke: true,
          color: getColor(engineer),
          opacity: 1,
          weight: 1,
          fill: true,
          fillColor: getColor(engineer),
          fillOpacity: 0.5
        }).bindPopup('Plan File: ' + row.PFN + '</br>' + 'Engineer: ' + row.Engineer + '</br>' + ' Date Received: ' + row.Date_Received + '</br>' + 'Status: ' + row.Status);

        // Add the marker only to its overlay
        marker.addTo(engineerLayers[engineer]);
      }

      map.addLayer(parentGroup);
      controlLayers.addTo(map);
    });

    // Must change these if engineers ever change
    function getColor(engineer) {
      switch (engineer) {
        case 'Mike':
          return 'green';
        case 'Salar':
          return 'blue';
        case 'Diego':
          return 'purple';
        case 'Saul':
          return 'orange';
        case 'Chan':
          return 'red';
        default:
          return 'black';
      }
    }

Here's the code I have written to allow the user to upload the CSV file and then to print the data:

    // Listen for file upload changes
    $('#csvFileUpload').change(function (e) {
      var file = e.target.files[0];
      var reader = new FileReader();

      // Read file contents
      reader.onload = function (e) {
        var csv = e.target.result;
        var markers = parseCSV(csv);

        // Add markers to the map
        markers.forEach(function (marker) {
          L.marker([marker.Latitude, marker.Longitude]).addTo(map)
            .bindPopup(marker.title);
        });
      };

      reader.readAsText(file);
    });

    // Parse CSV data into an array of objects
    function parseCSV(csv) {
      var lines = csv.split("\n");
      var result = [];
      var headers = lines[0].split(",");

      // Iterate through each line (excluding header) and create marker objects
      for (var i = 1; i < lines.length; i++) {
        var obj = {};
        var currentLine = lines[i].split(",");

        // Populate marker object with latitude, longitude, and title
        for (var j = 0; j < headers.length; j++) {
          obj[headers[j]] = currentLine[j];
        }

        result.push(obj);
      }

      return result;
    }

Here's some lines of my practice.csv:

PFN,Latitude,Longitude,Date_Received,District,Engineer,Status
0001-23-765-3000W,34.04395483,-118.1481822,1/23/2023,LA,Mike,Active
0002-23-3000W,34.04447045,-118.1509931,5/28/2003,Pomona,Salar,Not Active
0003-23NC,34.04271911,-118.1468947,9/30/1983,LA,Saul,Active
004-23-404-406,34.04264799,-118.1487937,2/2/1964,Pomona,Diego,Not Active
406,34.04210569,-118.1501992,6/6/1944,LA,Mike,Active
1249-23NC,34.0901146,-118.1486999,10/9/1924,Pomona,Salar,Not Active
1250-23NC,34.08822204,-118.151189,2/11/1905,LA,Saul,Active

UPDATE: My code now works great thanks to Ghybs answer, but for whatever reason, my code keeps creating a layer called undefined. I noticed in my previous code that an undefined layer would be created if a file with extra line spaces were included. Is there anyway to get rid of this so that the code is only reading the file with words on it rather then the extra space at the end? Here's what it looks like: enter image description here

I just added these lines to the function and it stops reading the last line:

    // Read file contents
  reader.onload = function (e) {
    var csv = e.target.result;
    var lines = csv.split("\n");
    var csvWithoutLastLine = lines.slice(0,-1).join("\n");
    parseCsv(csvWithoutLastLine);
    // Parse, build Markers and populate Layer Groups
    //parseCsv(csv);
  };

  reader.readAsText(file);
});

Solution

  • You can re-use most of your initial callback of $.get for handling user input CSV file as well (in particular re-using PapaParse instead of manually reading CSV). Mostly make sure to put Layer placeholder variables in outer shared scope, to re-use them as well:

    var engineerLayers = {}; // Create an object to hold the engineer subgroups
    var parentGroup = L.markerClusterGroup();
    map.addLayer(parentGroup);
    controlLayers.addTo(map);
    
    // Parse CSV string, build Markers and
    // populate Layer Groups
    function parseCsv(csvString) {
      var data = Papa.parse(csvString, {
        header: true,
        dynamicTyping: true
      }).data;
      
      for (var i in data) {
        var row = data[i];
        var engineer = row.Engineer;
    
        if (!(engineer in engineerLayers)) {
          // Each overlay that should actually be a part of MCG
          // should be made as a Subgroup, with the MCG as their parent
          engineerLayers[engineer] = L.featureGroup.subGroup(parentGroup, []);
          controlLayers.addOverlay(engineerLayers[engineer], engineer);
        }
    
        var marker = L.circleMarker([row.Latitude, row.Longitude], {
          radius: 10,
          stroke: true,
          color: getColor(engineer),
          opacity: 1,
          weight: 1,
          fill: true,
          fillColor: getColor(engineer),
          fillOpacity: 0.5
        }).bindPopup('Plan File: ' + row.PFN + '</br>' + 'Engineer: ' + row.Engineer + '</br>' + ' Date Received: ' + row.Date_Received + '</br>' + 'Status: ' + row.Status);
    
        // Add the marker only to its overlay
        marker.addTo(engineerLayers[engineer]);
      }
    });
    

    Then your initial file load becomes:

    $.get('\practice.csv', parseCsv);
    

    And your user CSV input handling:

    // Listen for file upload changes
    $('#csvFileUpload').change(function (e) {
      var file = e.target.files[0];
      var reader = new FileReader();
    
      // Read file contents
      reader.onload = function (e) {
        var csv = e.target.result;
        // Parse, build Markers and populate Layer Groups
        parseCsv(csv);
      };
    
      reader.readAsText(file);
    });