Last active
March 12, 2020 20:52
-
-
Save thomas-maschler/9104408a3f086733ea51d91e1a8e82f7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset='utf-8'/> | |
<title>Tree Cover Loss 3.0 Vector Tiles POC</title> | |
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no'/> | |
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.1.0/mapbox-gl.js'></script> | |
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.1.0/mapbox-gl.css' rel='stylesheet'/> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
} | |
#map { | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
width: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<style> | |
.map-overlay { | |
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; | |
position: absolute; | |
width: 25%; | |
top: 0; | |
left: 0; | |
padding: 10px; | |
} | |
.map-overlay .map-overlay-inner { | |
background-color: #fff; | |
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); | |
border-radius: 3px; | |
padding: 10px; | |
margin-bottom: 10px; | |
} | |
.map-overlay h2 { | |
line-height: 24px; | |
display: block; | |
margin: 0 0 10px; | |
} | |
.map-overlay input { | |
background-color: transparent; | |
display: inline-block; | |
width: 100%; | |
position: relative; | |
margin: 0; | |
cursor: ew-resize; | |
} | |
</style> | |
<div id='map'></div> | |
<div class="map-overlay top"> | |
<div class="map-overlay-inner"> | |
<h2>Tree cover loss base year</h2> | |
<label id="baseYear">2000</label> | |
<input id="slider" type="range" min="1" max="19" step="1" value="0"/> | |
</div> | |
</div> | |
<script> | |
// User can set baseline year from where to start monitoring loss. | |
// Any loss which occures before baseline year will be ignored | |
const initalBaseYear = 1; | |
// number of max loss occurrences per pixel. | |
// While this is 5 for pixels at native resolution it could be actually higher for aggreated pixels | |
// Set default to 20 | |
const n = 20; | |
// This is our default filter condition | |
// It checks if the given pixel has loss at recurrence <id> | |
// then it checks if the loss year is bigger or equal to our baseline year | |
var condition = (id, baseYear) => { | |
return [ | |
"all", | |
[">", ["length", ["get", "LossYear"]], id], | |
[">=", ["at", id, ["get", "LossYear"]], baseYear] | |
]; | |
}; | |
// for filters we either want true or false | |
var filterOutput = id => true; | |
const filterFallback = false; | |
// for intensity we want either the intensity at recurrence <id> divided by 100 | |
// or the fallback value 0 | |
var intensityOutput = id => { | |
return ["/", ["at", id, ["get", "LossIntensity"]], 100]; | |
}; | |
const intensityFallback = 0; | |
// for recurrence we want the number of recurrence and map it to a color ramp | |
// for recurrence we do not want to filter by baseline year | |
// there should always be at least 1 loss occurrence, so we can use this color as fallback value | |
var recurrenceOutput = id => { | |
return [ | |
"interpolate", | |
["linear"], | |
["get", "LossRecurrence"], | |
// ["length", ["get", "LossYear"]], | |
1, | |
"#FFCC00", | |
2, | |
"#F76000", | |
3, | |
"#FF0000", | |
4, | |
"#B51212", | |
5, | |
"#000000" | |
]; | |
}; | |
const recurrenceFallback = "#FFCC00"; | |
// for our labels we want the year at recurrence <id> | |
// or no label as fallback value | |
var labelOutput = id => { | |
return ["to-string", ["at", id, ["get", "LossYear"]]]; | |
}; | |
const labelFallback = ""; | |
// this function loops over all potential elements (max = n) and | |
// creates a 'case' expression, which returns the output value for the first condition that matches | |
// or the fallback value | |
var mapboxForLoop = (condition, output, fallback, baseYear) => { | |
let elements = [...Array(n).keys()]; | |
let expression = ["case"]; | |
elements.forEach(i => { | |
expression.push(condition(i, baseYear)); | |
expression.push(output(i)); | |
}); | |
expression.push(fallback); | |
console.log(JSON.stringify(expression)); | |
return expression; | |
}; | |
function changeBaseYear(baseYear) { | |
map.setPaintProperty('loss-year', "circle-opacity", mapboxForLoop( | |
condition, | |
intensityOutput, | |
intensityFallback, | |
baseYear | |
)); | |
map.setLayoutProperty('loss-year-labels', "text-field", mapboxForLoop( | |
condition, | |
labelOutput, | |
labelFallback, | |
baseYear | |
)); | |
// Set the label to the year | |
document.getElementById('baseYear').textContent = baseYear + 2000; | |
} | |
var map = new mapboxgl.Map({ | |
'container': 'map', | |
'zoom': 4, | |
'center': [5, 5], | |
'style': { | |
'version': 8, | |
'sources': { | |
'carto-dark': { | |
'type': 'raster', | |
'tiles': [ | |
"https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", | |
"https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", | |
"https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", | |
"https://d.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png" | |
] | |
}, | |
'tree-cover-loss': { | |
'type': 'geojson', | |
'data': { | |
// Dummy data | |
// this is waht the data structure of our vector tiles will look like | |
features: [ | |
{ | |
geometry: {coordinates: [25.074, -4.312], type: "Point"}, | |
properties: { | |
LossIntensity: [35, 50, 90], | |
LossYear: [4, 8, 16], | |
LossRecurrence: 3 | |
}, | |
type: "Feature" | |
}, | |
{ | |
geometry: {coordinates: [20.074, -6.312], type: "Point"}, | |
properties: { | |
LossIntensity: [80], | |
LossYear: [12], | |
LossRecurrence: 1 | |
}, | |
type: "Feature" | |
}, | |
{ | |
geometry: {coordinates: [10.754, 12.245], type: "Point"}, | |
properties: { | |
LossIntensity: [80, 60], | |
LossYear: [2, 7], | |
LossRecurrence: 2 | |
}, | |
type: "Feature" | |
} | |
], | |
type: "FeatureCollection" | |
} | |
} | |
}, | |
'layers': [{ | |
'id': 'carto-dark-layer', | |
'type': 'raster', | |
'source': 'carto-dark', | |
'minzoom': 0, | |
'maxzoom': 22 | |
}, // loss year layer | |
{ | |
id: "loss-year", | |
source: "tree-cover-loss", | |
type: "circle", | |
// filter: mapboxForLoop(condition, filterOutput, filterFallback), | |
paint: { | |
// There is a bug somewhere and this fails with a NAN error | |
// set this to 1 first change to the mapboxForLoop on runtime | |
// to overcome this issue | |
"circle-opacity": mapboxForLoop( | |
condition, | |
intensityOutput, | |
intensityFallback, | |
initalBaseYear | |
), | |
"circle-color": "#f69", | |
"circle-radius": 20 | |
} | |
}, // loss recurrence layer | |
// we still need a layer toggler, right now you will need to comment it in to show it | |
// { | |
// id: "loss-year-recurrence", | |
// type: "circle", | |
// // filter: mapboxForLoop(condition, filterOutput, filterFallback), | |
// paint: { | |
// // There is a bug somewhere and this fails with a NAN error | |
// // set this to 1 first change to the mapboxForLoop on runtime | |
// // to overcome this issue | |
// "circle-color": mapboxForLoop( | |
// condition, | |
// recurrenceOutput, | |
// recurrenceFallback), | |
// "circle-radius": 20 | |
// } | |
// }, | |
// this layer shows the loss year as labels, | |
// in the final app you would use the timeslider to filer | |
{ | |
id: "loss-year-labels", | |
source: "tree-cover-loss", | |
type: "symbol", | |
// filter: mapboxForLoop(condition, filterOutput, filterFallback), | |
layout: { | |
"text-field": mapboxForLoop( | |
condition, | |
labelOutput, | |
labelFallback, | |
initalBaseYear | |
), | |
"text-font": ["Open Sans Regular"], | |
"text-size": 16, | |
}, | |
paint: { | |
"text-color": "rgba(0,0,0,0.5)" | |
} | |
} | |
], | |
"glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf" | |
} | |
}); | |
map.addControl(new mapboxgl.NavigationControl()); | |
// When a click event occurs on a feature in the states layer, open a popup at the | |
// location of the click, with description HTML from its properties. | |
map.on('click', 'loss-year', function (e) { | |
console.log(e.features) | |
new mapboxgl.Popup() | |
.setLngLat(e.lngLat) | |
.setHTML("<strong>Tree Cover Loss Alert</strong>" + | |
"<p>Loss Years: " + e.features[0].properties.LossYear + "</p>" + | |
"<p>Loss Intensity: " + e.features[0].properties.LossIntensity + "</p>" + | |
"<p>Loss Recurrence: " + e.features[0].properties.LossRecurrence + "</p>") | |
.addTo(map); | |
}); | |
// Change the cursor to a pointer when the mouse is over the states layer. | |
map.on('mouseenter', 'loss-year', function () { | |
map.getCanvas().style.cursor = 'pointer'; | |
}); | |
// Change it back to a pointer when it leaves. | |
map.on('mouseleave', 'loss-year', function () { | |
map.getCanvas().style.cursor = ''; | |
}); | |
// Set filter to first month of the year | |
// 0 = January | |
// changeBaseYear(0); | |
document | |
.getElementById('slider') | |
.addEventListener('input', function (e) { | |
var baseYear = parseInt(e.target.value, 10); | |
changeBaseYear(baseYear); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment