See this blog post for context and details.
- Original idea by the New York Times (dataviz ; source code)
- Adapted to Leaflet by @pvernier
- Refactored and adapted to GeoJSON data extracted from a GPX file by @brunob
See this blog post for context and details.
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset=utf-8 /> | |
<title>Le Zigomar !</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" | |
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" | |
crossorigin=""/> | |
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" | |
integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" | |
crossorigin=""></script> | |
<script src="https://calvinmetcalf.github.io/leaflet-ajax/dist/leaflet.ajax.js"></script> | |
<style> | |
body { margin:0; padding:0; } | |
#map { position:absolute; top:0; bottom:0; width:100%; } | |
.info { padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 4px; } | |
.info p { margin-top: 0; } | |
.info #speed { background: #d7301f; height: 4px; width: 0; transition: all 0.5s ease; } | |
.glyphicon { display: inline-block; width: 16px; height: 16px; background-size: 16px; } | |
.glyphicon-globe { background-image: url(earth.svg); } | |
.glyphicon-target { background-image: url(target.svg); } | |
.glyphicon-play { background-image: url(play.svg); } | |
.glyphicon-pause { background-image: url(pause.svg); } | |
.glyphicon-replay { background-image: url(replay.svg); } | |
.glyphicon-time { background-image: url(time.svg); } | |
</style> | |
</head> | |
<body> | |
<div id="map"></div> | |
<script> | |
var bounds; | |
var map = L.map('map', { | |
center: [48.31356955685135, -4.523620605468751], | |
zoom: 10, | |
playing: false, | |
tracking: false | |
}); | |
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png', { | |
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', | |
subdomains: 'abcd', | |
maxZoom: 19 | |
}).addTo(map); | |
L.Util.ajax('2019_08_31.json').then(function(data){ | |
var bounds = L.geoJSON(data).getBounds(); | |
map.fitBounds(bounds); | |
var trackControl = L.control({position: 'topright'}); | |
trackControl.onAdd = function(map) { | |
var container = L.DomUtil.create('div', 'leaflet-bar'); | |
var animButton = L.DomUtil.create('a', 'glyphicon glyphicon-play', container); | |
animButton.setAttribute('role', 'button'); | |
animButton.title = 'Start, pause or replay the animation'; | |
animButton.setAttribute('aria-label', animButton.title); | |
animButton.setAttribute('id', 'animButton'); | |
// usefull ? | |
L.DomEvent.on(animButton, 'click', L.DomEvent.stopPropagation); | |
L.DomEvent.on(animButton, 'click', L.DomEvent.preventDefault); | |
// | |
L.DomEvent.on(animButton, 'click', function() { | |
// Play | |
if (L.DomUtil.hasClass(animButton, 'glyphicon-play') === true) { | |
L.DomUtil.setClass(animButton, 'glyphicon glyphicon-pause'); | |
map.playing = true; | |
window.requestAnimationFrame(addSegment); | |
} | |
// Pause | |
else if (L.DomUtil.hasClass(animButton, 'glyphicon-pause') === true) { | |
L.DomUtil.setClass(animButton, 'glyphicon glyphicon-play'); | |
map.playing = false; | |
} | |
// Replay | |
else { | |
L.DomUtil.setClass(animButton, 'glyphicon glyphicon-pause'); | |
map.playing = true; | |
i = 2; | |
window.requestAnimationFrame(addSegment); | |
} | |
}, this); | |
var trackButton = L.DomUtil.create('a', 'glyphicon glyphicon-target', container); | |
trackButton.setAttribute('role', 'button'); | |
trackButton.title = 'Click to track the boats. Click again to zoom out to full view'; | |
trackButton.setAttribute('aria-label', animButton.title); | |
// usefull ? | |
L.DomEvent.on(trackButton, 'click', L.DomEvent.stopPropagation); | |
L.DomEvent.on(trackButton, 'click', L.DomEvent.preventDefault); | |
// | |
L.DomEvent.on(trackButton, 'click', function() { | |
if (L.DomUtil.hasClass(trackButton, 'glyphicon-target') === true) { | |
L.DomUtil.setClass(trackButton, 'glyphicon glyphicon-globe'); | |
map.setView(position._latlng, 15); | |
map.tracking = true; | |
} else { | |
L.DomUtil.setClass(trackButton, 'glyphicon glyphicon-target'); | |
map.fitBounds(bounds); | |
map.tracking = false; | |
}; | |
}, this); | |
return container; | |
} | |
trackControl.addTo(map); | |
var maxSpeed = Math.max.apply(Math, data.features.map(function(o) { return o.properties.speed; })); | |
var info = L.control({position: 'bottomright'}); | |
info.onAdd = function (map) { | |
var div = L.DomUtil.create('div', 'info'); | |
// https://stackoverflow.com/a/4020842 / find max value in array with js | |
var time = new Date(data.features[0].properties.time.replace(/\//g, '-').slice(0, -3)).toTimeString().split(' ')[0]; | |
div.innerHTML = '<p id="chrono"><span class="glyphicon glyphicon-time" aria-hidden="true"></span> Time <span id="time">' + time +'</span></p><p>Speed (max ' + Math.round(maxSpeed * 1.94384 * 100) / 100 + ' knots)</p><p id="speed"></p>'; | |
return div; | |
}; | |
info.addTo(map); | |
var stylePoint = { | |
radius: 30, | |
fillColor: "#d7301f", | |
fillOpacity: 1, | |
color: "#d7301f" | |
}; | |
var point_1 = new L.LatLng(data.features[0].geometry.coordinates[1], data.features[0].geometry.coordinates[0]); | |
var point_2 = new L.LatLng(data.features[1].geometry.coordinates[1], data.features[1].geometry.coordinates[0]); | |
var pointList = [point_1, point_2]; | |
var trailLine = L.polyline(pointList, {color: '#d7301f', weight: 1.5}).addTo(map); | |
var trackLine = L.polyline(pointList, {color: '#d7301f', weight: 1, opacity: 0.2}).addTo(map); | |
var position = L.circle([data.features[0].geometry.coordinates[1], data.features[0].geometry.coordinates[0]], stylePoint).addTo(map); | |
var i = 2; | |
function draw(layer, polyline, position, splice) { | |
position._latlng.lat = layer.features[i].geometry.coordinates[1]; | |
position._latlng.lng = layer.features[i].geometry.coordinates[0]; | |
position.redraw(); | |
var point_n = new L.LatLng(layer.features[i].geometry.coordinates[1], layer.features[i].geometry.coordinates[0]); | |
if (polyline._latlngs.length < 11) { | |
polyline.addLatLng(point_n); | |
} | |
else { | |
if (splice) { | |
polyline.getLatLngs().splice(0, 1); | |
} | |
polyline.addLatLng(point_n); | |
} | |
if (map.tracking === true) { | |
map.setView(position._latlng, 15); | |
} | |
} | |
function addSegment() { | |
var time = L.DomUtil.get('time'); | |
var speed = L.DomUtil.get('speed'); | |
if (i++ < data.features.length - 1) { | |
draw(data, trailLine, position, true); | |
draw(data, trackLine, position, false); | |
if (i%6==0) { | |
time.innerHTML = new Date(data.features[i].properties.time.replace(/\//g, '-').slice(0, -3)).toTimeString().split(' ')[0]; | |
speed.setAttribute('style', 'width: ' + data.features[i].properties.speed * 100 / maxSpeed + '%'); | |
} | |
} else { | |
map.playing = false; | |
L.DomUtil.setClass(L.DomUtil.get('animButton'), 'glyphicon glyphicon-replay'); | |
trailLine.getLatLngs().splice(0, 11); | |
L.DomUtil.get('speed').setAttribute('style', 'width: 0'); | |
} | |
if(map.playing){ | |
window.requestAnimationFrame(addSegment); | |
} | |
} | |
}); | |
</script> | |
</body> | |
</html> |