Creating a Web Mapping Application with Leaflet JS

Web mapping applications are powerful tools that enable users to visualize and interact with geographical data. In this tutorial, we'll explore how to build a simple yet effective mapping application using Leaflet JS, a popular JavaScript library for interactive maps. 

Leaflet JS is a lightweight, open-source library for creating mobile-friendly interactive maps. It provides a simple and intuitive API for incorporating maps into web pages.

A client in FIVVER (if you needs my service for your GIS Job, the FIVVER link on the left top bar of my blog) asked me to make a simple webmapping application using Leaflet JS that has functionality to zoom and pan in seamless way based on the attribute table selection.

The data is stored as GEOJSON file and directly injected in the HTML, so the HTML webmap can be opened and distributed in various computer at ease.

I design this webmap with simplicity mindset, so reproduction into another website can be done easily.

If you are curious about the code and probably consider to use it for your own purposes, here is the code


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CY2024 Health Equity Learning Collaborative Participants Viewer </title>
  <!-- Include Leaflet CSS and JS -->
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css">
  <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
  <style>
    /* Set the map container to fill the entire screen */
    #map {
      height: 100vh;
      width: 100%;
    }

    .attribute-table {
      max-height: 400px;
      overflow-y: auto;
      padding: 10px;
      width: 400px; /* Set the width of the table container */
    }

    .attribute-table table {
      border-collapse: collapse;
      border-spacing: 0;
      width: 100%;
    }

    .attribute-table th,
    .attribute-table td {
      border: 1px solid #ddd;
      padding: 12px;
      text-align: left;
      cursor: pointer;
      word-wrap: break-word; /* Allow content to wrap within cells */
    }

    .attribute-table th {
      background-color: #87CEEB; /* Light Blue */
      color: #fff; /* White text color */
      position: sticky;
      top: -10px;
      z-index: 2; /* Increase the z-index to keep the header above the tbody */
    }

    .attribute-table tr:hover {
      background-color: #f5f5f5;
    }

    .attribute-table thead,
    .attribute-table tbody {
      display: table;
      width: calc(100% - 17px); /* Adjust for scrollbar width */
    }

    .attribute-table thead {
      display: table-header-group;
    }

    .attribute-table tbody {
      display: table-row-group;
    }

    .attribute-table tbody tr {
      background-color: #fff;
    }

    .attribute-table tbody tr.green {
      background-color: green;
      color: #fff; /* White text color for green background */
    }

    .attribute-table tbody tr.yellow {
      background-color: yellow;
      color: #000; /* Black text color for yellow background */
    }

    .attribute-table th:nth-child(1),
    .attribute-table td:nth-child(1) {
      width: 50%; /* Adjust the width as needed */
    }

    .attribute-table th:nth-child(2),
    .attribute-table td:nth-child(2) {
      width: 50%; /* Adjust the width as needed */
    }

    .no-scroll-map {
      pointer-events: none;
    }
  .legend {
    position: bottomleft;
    background-color: white;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
    font-size: 14px;
  }

  .legend h4 {
    margin-top: 0;
    margin-bottom: 10px;
  }

  .legend-item {
    display: flex;
    align-items: center;
    margin-bottom: 8px;
  }

  .legend-color {
    width: 20px;
    height: 20px;
    margin-right: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
  }
  </style>
</head>
<body>

  <!-- Create a container for the map -->
  <div id="map"></div>

  <script>
    var map = L.map('map', {
      center: [39.748824, -84.209605],
      zoom: 4,
    });

  // Enable scroll wheel zoom when the map is loaded
  map.on('load', function () {
    map.scrollWheelZoom.enable();
  });
  


    // Add a base map layer (you can choose a different one if you like)
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
    }).addTo(map);

    // Parse the GeoJSON data
    var geojson_data = {


    // Define a GeoJSON layer and add it to the map
    var geojsonLayer = L.geoJSON(geojson_data, {
      pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, {
          radius: 8,
          fillColor: 'green',
          color: '#000',
          weight: 1,
          opacity: 1,
          fillOpacity: 0.8
        });
      },
      onEachFeature: function (feature, layer) {
        // Popup with information from GeoJSON properties
        var popupContent = "<p><b>Organization:</b> " + feature.properties.Organizati + "</p>" +
                           "<p><b>Address:</b> " + feature.properties.Address_1 + "</p>";
        layer.bindPopup(popupContent);

        // Hover popup showing only Org_name and Address_1
        layer.on('mouseover', function (e) {
          this.openPopup();
        });

        layer.on('mouseout', function (e) {
          this.closePopup();
        });
      }
    });

 

    geojsonLayer.addTo(map);


    // Function to process a GeoJSON layer and add rows to the attribute table
    function processGeojsonLayer(layer, tbody, layerClass) {
      layer.eachLayer(function (layer) {
        var row = L.DomUtil.create('tr', layerClass, tbody);

        // Add Organization cell
        var orgCell = L.DomUtil.create('td', '', row);
        orgCell.innerHTML = layer.feature.properties.Organizati;
        orgCell.addEventListener('click', function () {
          map.flyTo(layer.getLatLng(), 15, {
            duration: 2,
            easeLinearity: 0.25,
          });
        });

        // Add Address cell
        var addressCell = L.DomUtil.create('td', '', row);
        addressCell.innerHTML = layer.feature.properties.Address_1;
        addressCell.addEventListener('click', function () {
          map.flyTo(layer.getLatLng(), 15, {
            duration: 2,
            easeLinearity: 0.25,
          });
        });

        // Set the row background color to match the circle color
        row.style.backgroundColor = layer.options.fillColor;
      });
    }

    // Create a custom control for the attribute table
    var attributeControl = L.control({ position: 'bottomright' });

    attributeControl.onAdd = function (map) {
      var div = L.DomUtil.create('div', 'info attribute-table');
      var table = L.DomUtil.create('table', '', div);
      var thead = L.DomUtil.create('thead', '', table);
      var tbody = L.DomUtil.create('tbody', '', table);

      var headerRow = L.DomUtil.create('tr', '', thead);
      L.DomUtil.create('th', '', headerRow).innerHTML = 'Organization';
      L.DomUtil.create('th', '', headerRow).innerHTML = 'Address';

      // Process geojsonLayer initially
      processGeojsonLayer(geojsonLayer, tbody, 'green');



      return div;
    };

    attributeControl.addTo(map);

    // Create a layer control with base map and GeoJSON layer
    var baseMaps = {
      "OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors'
      })
    };

    var overlayMaps = {
      "CY2024 HELC Participants": geojsonLayer,
 
    };

    L.control.layers(baseMaps, overlayMaps, { collapsed: false }).addTo(map);

    // Add event listeners to enable/disable scroll wheel zoom based on cursor position
    var attributeTable = document.querySelector('.attribute-table');

    attributeTable.addEventListener('mouseenter', function () {
      map.scrollWheelZoom.disable();
    });

    attributeTable.addEventListener('mouseleave', function () {
      map.scrollWheelZoom.enable();
    });

    // Listen for overlay add/remove events to update the attribute table
    map.on('overlayadd', function (event) {
      if (event.layer === geojsonLayer || event.layer === geojsonLayer2) {
        updateAttributeTable();
      }
    });

    map.on('overlayremove', function (event) {
      if (event.layer === geojsonLayer || event.layer === geojsonLayer2) {
        updateAttributeTable();
      }
    });

    // Function to update the attribute table based on the currently visible layers
    function updateAttributeTable() {
      // Clear the existing table rows
      var tbody = document.querySelector('.attribute-table tbody');
      tbody.innerHTML = '';

      // Process geojsonLayer if it is visible
      if (map.hasLayer(geojsonLayer)) {
        processGeojsonLayer(geojsonLayer, tbody, 'green');
      }

      // Process geojsonLayer2 if it is visible
      if (map.hasLayer(geojsonLayer2)) {
        processGeojsonLayer(geojsonLayer2, tbody, 'yellow');
      }
    }
	
  // Create a legend control
  var legendControl = L.control({ position: 'bottomleft' });

  legendControl.onAdd = function (map) {
    var legendDiv = L.DomUtil.create('div', 'legend');

    var legendContent = '<h4>Legend</h4>';

    // Legend items
    var legendItems = [
      { color: 'green', label: 'CY2024 Health Equity Learning Collaborative Participants' },

    ];

    legendItems.forEach(function (item) {
      legendContent += '<div class="legend-item">';
      legendContent += '<div class="legend-color" style="background-color: ' + item.color + '"></div>';
      legendContent += '<div>' + item.label + '</div>';
      legendContent += '</div>';
    });

    legendDiv.innerHTML = legendContent;

    return legendDiv;
  };

  legendControl.addTo(map);

  </script>

</body>
</html>

Just feed your GEOJSON data in the geojson_layer variable, set the popup attribute according to your preferred fields list, and you are good to go. Here is a preview of the finished app.

LIVE DEMO can be accessed from this LINK.



If you find an error in using the CODE, mail me at mappingsince2004@gmail.com

Feel free to customize and expand upon this foundation to meet your specific needs. Leaflet JS offers a rich set of features for creating dynamic and interactive maps, making it a powerful tool for a wide range of mapping applications.

Happy mapping!





Comments

Popular posts from this blog

Daftar Alamat dan Link Geoportal dan GIS Portal Kementerian, Lembaga, Badan dan Pemerintah Daerah di Indonesia

HAE (Height Above Ellipsoid) and MSL (Mean Sea Level) Conversion Using Pathfinder Office

TUTORIAL ORTHOREKTIFIKASI CITRA SATELIT RESOLUSI SEDANG (CITRA ASTER)