10b: Leaflet / 2.5D / 3D Maps

Objectives

  • Integrate D3 layers with Leaflet and a map tiling platform (e.g. Open Street Maps)
  • Look at a simple 3D example using Mapbox.

What is Leaflet?

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

Installing Leaflet

Let's use the assignment 4 starter as a base template.

We'll also need the SG GeoJSON dataset.

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


<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.css" />
                    

<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
                    

Load Leaflet basemap

Ignore / comment out the D3 code for now.

You'll need to create a map container for Leaflet to hook to.

Make sure you can get Leaflet and the base map tiles to load.


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

<div id="map"></div>
                    

let tiles = new L.TileLayer("http://{s}.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 maxBounds


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);
                    

Try adding in parameters like maxZoom and minZoom. Find out more on the Leaflet docs.

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 Leaflet providers

Leaflet ideas

You can already 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.34128961848499, 103.96340773679763]).addTo(map);
    marker.bindPopup("SUTD campus");
                        

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 the 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 maps using Mapbox

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

Comparison here.

Please go get your own mapbox API key - it's free for low amount of usage.

We're going to run this example from mapbox.

All 2.5D mapbox code will be run using your own API key!

Mapbox - embed 3D object

We're going to embed a 3D object. We'll use three.js to load it, then render it using mapboxGL canvas renderer.

We'll run this example from mapbox.

Extra: Will demo an example locally, using three.js code to load animation.

3D - future perspectives

deck.gl and Mapbox GL JS: Better Together, Xiao Ji Chen

Our Thoughts as MapboxGL JS v2.0 Goes Proprietary, Carto

Cesium Sandcastle

deck.gl Examples

Questions?

Chi-Loong | V/R