Mapbox GL JS 3D buildings

Here’s another mapbox gl js example – this time using Mapbox’s new ability to visualize 3-dimensional data. I found 3D massing data of buildings in Toronto on the City of Toronto’s open data site. Buildings are coloured based on their height, and a click event creates a pop up with the clicked building’s height.

Here’s the javascript, followed by the html:

Javascript

mapboxgl.accessToken = //<your access code here>;

var map = new mapboxgl.Map({
 container: 'map',
 style: 'mapbox://styles/mwidener/ciuvowcno00ee2js5wibckxv3',
 center: [-79.388924,43.645],
 zoom: 14,
 minZoom: 13, //restrict map zoom - buildings not visible beyond 13
 maxZoom: 20,
 pitch: 60, //tilt camera
 bearing: 17.5 //adjust angle we're looking (17.5 degrees from north)
});

var features=[];

var layers = [
 //Color Height threshold Label
 ['#ffffcc', 15, '0-15 Meters'],
 ['#ffeda0', 30, '15-30 Meters'],
 ['#fed976', 50, '30-50 Meters'],
 ['#feb24c', 100, '50-100 Meters'],
 ['#fd8d3c', 200, '100-200 Meters'],
 ['#fc4e2a', 300, '200-300 Meters'],
 ['#e31a1c', 400, '300-400 Meters'],
 ['#bd0026', 500, '400-500 Meters']
];

//create a popup object and give it some properties. we'll generate/overwrite a new popup each time we click.
var popup = null;

map.on('style.load', function(){
 var legend = document.getElementById('legend'); //get the legend from the HTML document
 map.addSource('building_source', {
 'url': 'vector',
 'url': 'mapbox://mwidener.1ayxbus1' //location of 3d building data
 });

 layers.forEach(function(layer, i){
 map.addLayer({
 'id': 'building_layer-' + i,
 'type': 'fill',
 'source': 'building_source',
 'paint': {
 'fill-color': layer[0],
 'fill-opacity': 0.6,
 'fill-extrude-height': { //grab the height from the 'EleZ' attribute from building data
 'type': 'identity',
 'property': 'EleZ'
 },
 'fill-extrude-base': 0
 },
 'source-layer': 'Archive-bcz1gm' //source layer of 3d building data on mapbox studio
 });
 // Build out legends
 //***********//
 var item = document.createElement('div');//each layer gets a 'row' - this isn't in the legend yet, we do this later
 var key = document.createElement('span');//add a 'key' to the row. A key will be the color circle
 var value = document.createElement('span');//add a value variable to the 'row' in the legend

 key.className = 'legend-key'; //the key will take on the shape and style properties defined here, in the HTML
 key.style.backgroundColor = layer[0]; // the background color is retreived from teh layers array

 value.id = 'legend-value-' + i; //give the value variable an id that we can access and change

 item.appendChild(key); //add the key (color cirlce) to the legend row
 item.appendChild(value);//add the value to the legend row

 legend.appendChild(item); // Add row to the legend

 });

 layers.forEach(function(layer, i) {
 var filters = ['all',['<=', 'EleZ', layer[1]]];
 if (i !== 0) filters.push(['>', 'EleZ', layers[i - 1][1]]);
 map.setFilter('building_layer-' + i, filters);
 document.getElementById('legend-value-' + i).textContent = layer[2];//.toLocaleString(); //as we iterate through the layers
 //get the correct row, and add the appropriate text //get the correct row, and add the appropriate text
 });
});

//create an array that has the name of all layers created above
var all_added_layers = []
layers.forEach(function(layer,i){
 all_added_layers.push('building_layer-'+i);
})

map.on('mousemove', function(e){
 features = map.queryRenderedFeatures(e.point, {all_added_layers}); //get features that are in building layers
 if (features.length > 0){
 console.log(features[0].id);
 //use the following code to change the cursor to a pointer ('pointer') instead of the default ('')
 map.getCanvas().style.cursor = (features[0].properties.EleZ != null) ? 'pointer' : '';
 }
 //if there are no provinces under our mouse, then change the cursor back to the default
 else {
 map.getCanvas().style.cursor = '';
 }
});

//an event where when there is a mouse click, send the event data (represented by e) to a function that does something
map.on('click', function(e) {
 popup = new mapboxgl.Popup({
 closeButton: true,
 closeOnClick: true
 });

 //get the spatial features where your mouse is currently located. note we use the pixel location (e.point) and not lat/lon here.
 //also specify the feature we want to pay attention to - 'provinces_base'
 var features = map.queryRenderedFeatures(e.point, {layers: all_added_layers});
 if (features.length > 0 && features[0].properties.EleZ != null){ //if there is a feature in our features array then proceed. otherwise, we don't have anything to work with
 //center at click locations
 map.panTo(e.lngLat, {'duration': 1000});
 //set the location of our popup to the lnglat of our click (note we use e.lnglat here and NOT e.point)
 popup.setLngLat(e.lngLat);
 //give the popup content
 popup.setHTML(
 '<center>The height of this building is approximately: <b>' + Math.round(features[0].properties.EleZ) + '</b> meters.'
 );
 //finally add the popup to the map
 popup.addTo(map);
 }

 //if there are no features that we're interested in, reset the filter for provinces_hover and remove any popup.
 else{
 //map.setFilter('provinces_hover',['==','PRVNAME','']);
 popup.remove();
 }

});

HTML

<!DOCTYPE html>
<html>
<head>
 <meta charset=utf-8 />
 <title>Toronto Skyline</title>
 <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
 <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.js'></script>
 <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.css' rel='stylesheet' />
 <style>
 body { margin:0; padding:0; }
 #map { position:absolute; top:0; bottom:0; width:100%; }
 </style>
 <style>
 .legend-container {
 position: absolute;
 bottom: 0px;
 right: 0px;
 padding: 0px 10px;
 margin-bottom: 30px;
 z-index: 1;
 }
 .legend {
 font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
 background-color: white; /*background color of legend*/
 padding: 10px; /*around stuff inside of legend*/
 border-radius: 10px; /*round the edges of the legend div*/
 box-shadow: 10px 10px 20px rgba(0,0,0,0.60); /*horizontal shadow, vertical shadow, size of shadow, color*/
 }
 .legend h2 {
 margin: 0px 0px 5px; /*top margin, left/right margin, bottom margin*/
 }
 .legend-key {
 display: inline-block;
 border-radius: 50%; /*curve border ... make a circle*/
 width: 20px; /*size of circle*/
 height: 20px;
 margin-right: 10px; /*position of circle*/
 }
 </style>

</head>
<body>
<div id='map' class='map'> </div>
<div class='legend-container'>
 <div id='legend' class='legend'>
 <h2>Height</h2>
 <i>Use right mouse button to navigate 3d space.
 <br>Click on a building to see its height.</i>
 <!-- All the other legend stuff will be added here as we programatically insert divs/spans-->
 </div>
</div>
<script src='./TorontoBuildings.js'></script>
</body>
</html>

Mapbox GL JS – Click and Hover Events

We just went over events in my mapbox gl js class at the University of Toronto. For fun, I updated the “GeoCV” map on my homepage to add these features. Here’s the JS code:

//Javascript File
mapboxgl.accessToken = '<INSERT YOUR ACCESS KEY HERE>';
var popup = null
var map = new mapboxgl.Map({
 container: 'map',
 style: 'mapbox://styles/mwidener/ciukh23bo000b2io4qrcq87s9',
 center: [-84.516476, 39.131658],
 zoom: 2.5
});

//academic_positions is a vector tile file stored on mapbox studio
//~~~FEATURES:~~~~
//Academic_Positions
//3 properties | This layer contains mostly Points
//Name  String
//Position  String
//Time Period  String

map.on('style.load', function(){
 map.addSource('academic_positions_src',{
 'type': 'vector',
 'url': 'mapbox://mwidener.ciulhf3s100662zmwys884b2b-0d75j'
 });

//display two layers ... both drawing from the vector tiles from 
//source: 'academic_provinces_src'
 map.addLayer({
 'id': 'academic_positions',
 'type': 'circle',
 'source': 'academic_positions_src',
 'interactive': true,
 'layout': {},
 'paint': {
 'circle-color': 'red',
 'circle-radius': 5,
 },
 'source-layer': 'Academic_Positions'
 });

 map.addLayer({
 'id': 'academic_positions_clicked',
 'type': 'circle',
 'source': 'academic_positions_src',
 'interactive': true,
 'layout': {},
 'paint': {
 'circle-color': 'red',
 'circle-radius': 6, //make point a little bigger after click
 },
 'source-layer': 'Academic_Positions',
 'filter': [ '==', 'Name', '' ] 
// start with a filter that doesn't select anything
 });

});

//an event where when there is a mouse click, 
//send the event data (represented by e) to a 
//function that does something

map.on('click', function(e) {
 //get the spatial features where your mouse is currently located. 
 //note we use the pixel location (e.point) and not lat/lon here.
 //also specify the feature we want to pay attention 
 //to - 'academic_positions'
 var features = map.queryRenderedFeatures(e.point, {
 layers: ['academic_positions']
 });
 if (!features.length) {
 return;
 }

 popup = new mapboxgl.Popup({
 closeButton: true,
 closeOnClick: true
 });

 //set a filter on the academic_positions_clicked layer 
 //so that the point we clicked on shows up
 map.setFilter('academic_positions_clicked',['==','Name',features[0].properties.Name]);
 //set the location of our popup to the 
 //lnglat of our click (note we use e.lnglat here and NOT e.point)
 popup.setLngLat(e.lngLat);
 //give the popup content
 popup.setHTML(
 "<b>" + features[0].properties.Name + "</b>" + 
 "<br>Michael's position: " + features[0].properties.Position +" "
 );
 //finally add the popup to the map
 popup.addTo(map);
 map.panTo(e.lngLat);
 });

//When the mouse moves over a spatial layer 
//we care about (e.g. a point) let's change the mouse cursor
map.on("mousemove", function(e) {
 //get the province feature underneath the mouse
 var features = map.queryRenderedFeatures(e.point, {
 layers: ["academic_positions"]
 });
 //if there's a point under our mouse, then do the following.
 if (features.length > 0) {
 //use the following code to change the 
 //cursor to a pointer ('pointer') instead of the default ('')
 map.getCanvas().style.cursor = (features[0].properties.Name !== null) ? 'pointer' : '';
 }
 //if there are no points under our mouse, 
 //then change the cursor back to the default
 else {
 map.getCanvas().style.cursor = '';
 }
});

 

And here’s the HTML:

<!DOCTYPE html>
<html>
<head>
 <meta charset=utf-8 />
 <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
 <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.js'></script>
 <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.26.0/mapbox-gl.css' rel='stylesheet' />
 <style>
 body { margin:0; padding:0; }
 #map { 
 position:absolute; 
 top:0; 
 bottom:0; 
 width:100%; 
 }
 .map-overlay {
 position: absolute;
 bottom: 0;
 right: 0;
 background: rgba(255, 255, 255, 0.8);
 margin-right: 10px;
 margin-bottom:24px;
 padding-left: 3px;
 padding-right:3px;
 padding-top:3px;
 padding-bottom:3px;
 font-family: Arial, sans-serif;
 border-radius: 5px;
 }


 </style>
</head>
<body>
 <div id='map' class='map'> </div>
 <!--<div id='map'></div>-->
 <div class='map-overlay' id='features'><b><center> Click on a red point <br> to see where <br> Michael has been! </b></center></div>
 <script src='./geocv.js'></script>
</body>
</html>