Skip to content

Instantly share code, notes, and snippets.

@thomas-maschler
Last active March 12, 2020 20:52
Show Gist options
  • Save thomas-maschler/9104408a3f086733ea51d91e1a8e82f7 to your computer and use it in GitHub Desktop.
Save thomas-maschler/9104408a3f086733ea51d91e1a8e82f7 to your computer and use it in GitHub Desktop.
<!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