10b: Leaflet / 2.5D / 3D Maps

Objectives

  • Using Leaflet and a map tiling platform (e.g. Open Street Maps)
  • Look at a simple 3D example using Maplibre (the open source fork of Mapbox).

What is Leaflet?

Leaflet is the leading open-source JavaScript library for mobile-friendly interactive maps. Weighing just about 42 KB of JS, it has all the mapping features most developers ever need.

Installing Leaflet

We're going to add Leaflet CSS and JS via a CDN.


<html>
<head>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
  <style></style>
</head>
<body>
  <div id="map"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
  <script></script>
</body>
</html>
                    

Load Leaflet basemap

We're using openstreetmap tiles here.

Note that Leaflet is hooked onto the map container defined previously.


#map {
    width: 1000px;
    height: 600px;
}
                    

let tiles = new L.TileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
    attribution: 'Tiles © OpenStreeMaps'
});

let map = new L.Map("map", {
        center: [1.347833, 103.809357], 
        zoom: 11,
    })
    .addLayer(tiles);
                    

Add bounds and zoom parameters


let map = new L.Map("map", {
        center: [1.347833, 103.809357], 
        zoom: 11,
        maxBounds: L.latLngBounds(L.latLng(1.1, 103.5), L.latLng(1.5, 104.3))
    })
    .addLayer(tiles);
                    

More on zoom levels

Try adding in parameters like maxZoom and minZoom

Scale control

Add in the Leaflet scale control so we can see the zoom.


L.control.scale().addTo(map);
                    

You can control the zoom / pan using code rather than using interface

Example: Exploring the lungs of Asia, Kontinentalist


    setTimeout(function(){
      map.flyTo([1.34141372, 103.963757], 15)
    }, 2000);
                    

Other basemaps: Google Tiles


let tiles = new L.tileLayer('http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',{
    subdomains:['mt0','mt1','mt2','mt3'],
    attribution: 'Tiles © Google'
});
                    

Other basemaps: ESRI ArcGISOnline


let tiles = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
    attribution: 'Tiles © Esri'
});
                    

Map styles

For many base maps you need to register or install their script.

Here's a small list of map providers

Leaflet markers

You can do a lot of things within Leaflet and base map tiles.

Leaflet Examples


Can you add a marker location at the SUTD campus on your map?


    let marker = L.marker([1.34128961, 103.96340773]).addTo(map);
    marker.bindPopup("SUTD campus");
                        

Try adding lines and circles to the Leaflet map!

Leaflet: geojson

Useful reading: Leaflet GeoJSON tutorial.

Let's go through this snippet of code.

Leaflet: choropleth

You can do Assignment 4 using Leaflet instead.

Useful reading: Leaflet Choropleth tutorial.

Bootcamp Day 3 final project is assignment 4 done in Leaflet instead.

Can we integrate Leaflet and D3?

Of course you can!

Hook D3 with Leaflet

Hook a SVG layer into Leaflet's overlayPane.


let svg = d3.select(map.getPanes().overlayPane)
    .append("svg")
    .attr("width", 1000)
    .attr("height", 600)
        .append("g")
        .attr("id","svgLayer")
        .attr("class", "leaflet-zoom-hide");
                    

The leaflet-zoom-hide class is so that when you use Leaflet zoom, it turns of the layer temporarily during the zooming animation.

You can try what happens when you don't include this in the final instance.

Set SVG projection to use Leaflet

You cannot use D3's Mercator projection. The projection needs to be the same as Leaflet's.

Leaflet uses its own geo projection, and we're going to make D3 sync with that.

Recall that D3 projection is simply taking a lat/lon coordinate and turning it into a SVG x, y point.


function projectPoint(x, y) {
  var point = map.latLngToLayerPoint(new L.LatLng(y, x));
  this.stream.point(point.x, point.y);
}

let projection = d3.geoTransform({point: projectPoint})
let geopath = d3.geoPath().projection(projection);
                    

Update D3 redraw

Looks good. But zooming and scaling breaks the visualization.

We're going to write a function recalculate the bounds and redraw the SVG when moved...


function redrawLeafletLayer() {
    // d3.geo.bounds takes a single Feature or a FeatureCollection as argument
    let bounds = geopath.bounds(data[0]),
        topLeft = bounds[0],
        bottomRight = bounds[1];

    let svg = d3.select(map.getPanes().overlayPane).select("svg");
        
    svg.attr("width", bottomRight[0] - topLeft[0])
       .attr("height", bottomRight[1] - topLeft[1])
       .style("left", topLeft[0] + "px")
       .style("top", topLeft[1] + "px");
       
    svg.select("g").attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
        
    d3.select("#map svg g#districts").selectAll("path")
       .attr("d", geopath);
}                    
                    

Update Leaflet zoomend hook

...Then we're going to call this function every time Leaflet is zoomed.


  redrawLeafletLayer();
  // reset whenever map is moved
  map.on('zoomend', redrawLeafletLayer);
                    

ObservableHQ on how to use D3 with Leaflet.

Leaflet - mouse interaction

Leaflet turns off mouse events for all overlay SVG panes by default.

Add the "leaflet-interactive" class to every SVG tag (the appended paths in the case of districts) that you want to make interactive.

2.5D (2D shapes + height) vs 3D

Mapbox (2.5D) isn't a true 3D solution, say unlike Cesium (3D).

Comparison here.

MapLibre 3D model example

MapLibre is a free open-source fork of MapboxGL

We're going to embed a 3D model onto a basemap, using this snippet of code.

Please download it and try to run it.

The code loads a 3D object using three.js and places it on a map using MapLibre.

Replace base map

If you want a glassy, fast basemap, you have to pay for it. E.g. Maptiller, Mapbox, etc.

The API code given in the code snippet for maptiller will not work (of course). Let's just use openstreemaps.



        style: {
          version: 8,
          sources: {
              osm: {
                  type: 'raster',
                  tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
                  tileSize: 256,
                  attribution: '© OpenStreetMap Contributors',
              }
          },
          layers: [
              {
                  id: 'osm',
                  type: 'raster',
                  source: 'osm'
              }
          ]
        },
                    

3D - future perspectives

SG elections, D3 + OSM + mapLibre

Viz.gl, Uber viz open-source framework

Cesium Sandcastle examples

deck.gl examples

Questions?

Chi-Loong | V/R