Last active
April 11, 2023 08:09
-
-
Save tkardi/99fb0a96b12068157af30cf806651bcf to your computer and use it in GitHub Desktop.
Code companion to 'Building a flightradar in Leaflet (pt II)'
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> | |
<!-- The following HTML page together with the accompanying | |
javascript is published under the Unlicense. | |
SPDX-License-Identifier: Unlicense | |
--> | |
<head> | |
<meta charset="utf-8"> | |
<title>Air traffic map</title> | |
<link | |
rel="stylesheet" | |
type="text/css" | |
href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.css"/> | |
<script | |
src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.js" | |
type="text/javascript"> | |
</script> | |
<script | |
src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.0.0/leaflet-realtime.min.js" | |
type="text/javascript"> | |
</script> | |
<style> | |
#map { | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
right: 0; | |
} | |
.aeroplane { | |
opacity: 0; | |
} | |
.aeroplane-visible { | |
background: #109856; | |
border: none; | |
opacity: 1.0; | |
} | |
.aeroplane-visible.end{ | |
transition: opacity 5s ease-in-out; | |
opacity: 0.01; | |
} | |
.radar-hand { | |
filter: url(#blur); | |
} | |
</style> | |
</head> | |
<body> | |
<div id="map"> | |
<svg height="140" width="140"> | |
<defs> | |
<filter id="blur" x="0" y="0" width="100%" height="100%"> | |
<feGaussianBlur result="blurOut" in="SourceGraphic" stdDeviation="5" /> | |
</filter> | |
</defs> | |
</svg> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.0.0/leaflet-realtime.min.js" type="text/javascript"></script> | |
<script> | |
var center = [58.65, 25.06], | |
radarbeam = {"type":"LineString", "coordinates": [[center[1], center[0]], [center[1], center[0]]]}; | |
var map = L.map('map').setView(center, 7); | |
var realtime = L.realtime('http://127.0.0.1:5000/flightradar', { | |
interval: 10 * 1000, | |
getFeatureId: function(feature) { | |
return feature.id; | |
}, | |
pointToLayer: function(feature, latlng) { | |
var marker = L.marker(latlng, { | |
icon: L.divIcon({ | |
className:'aeroplane', | |
iconSize: [10,10] | |
}), | |
riseOnHover: true | |
}).bindTooltip( | |
'<b>{callsign}</b><br>Alt: {geo_altitude} m @ {velocity} m/s'.replace( | |
L.Util.templateRe, function (str, key) { | |
var value = feature.properties[key]; | |
if (value === undefined || value == null) { | |
value = 'N/A'; | |
} | |
return value; | |
}), | |
{ | |
permanent: false, opacity: 0.7} | |
); | |
return marker; | |
}, | |
pointInPolygon: function (latlng, latlngs) { | |
// with a slight modifications taken from | |
// https://github.com/substack/point-in-polygon/blob/master/index.js | |
// which is licensed under the MIT license | |
// https://github.com/substack/point-in-polygon/blob/master/LICENSE | |
var x = latlng.lng, y = latlng.lat; | |
var inside = false; | |
for (var i = 0, j = latlngs.length - 1; i < latlngs.length; j = i++) { | |
var xi = latlngs[i].lng, yi = latlngs[i].lat; | |
var xj = latlngs[j].lng, yj = latlngs[j].lat; | |
var intersect = ((yi > y) != (yj > y)) | |
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
if (intersect) inside = !inside; | |
} | |
return inside; | |
} | |
}).addTo(map); | |
// Add polar craticule | |
var rings = []; | |
[50000, 100000, 150000, 200000, 250000].forEach(function(r) { | |
rings.push( | |
L.circle( | |
center, { | |
radius: r, | |
fill: false, | |
weight: r % 100000 == 0 ? 1.75 : 0.75, | |
color: '#808080' | |
} | |
).addTo(map) | |
); | |
}); | |
var xy1 = map.options.crs.project(L.latLng(center)), | |
radius = 550000; | |
var right = L.point(xy1).add([radius, 0]), | |
left = L.point(xy1).subtract([radius, 0]), | |
tops = L.point(xy1).add([0, radius]), | |
bottom = L.point(xy1).subtract([0, radius]); | |
var crosshairs = [ | |
L.polyline([map.options.crs.unproject(left), map.options.crs.unproject(right)], {weight: 1.5, color: '#808080'}).addTo(map), | |
L.polyline([map.options.crs.unproject(tops), map.options.crs.unproject(bottom)], {weight: 1.5, color: '#808080'}).addTo(map), | |
]; | |
[45, 135, 225, 315].forEach(function(angle) { | |
crosshairs.push( | |
L.polyline([ | |
map.options.crs.unproject(L.point([ | |
xy1.x + Math.sin(angle * Math.PI / 180) * 75000, | |
xy1.y + Math.cos(angle * Math.PI / 180) * 75000 | |
])), | |
map.options.crs.unproject(L.point([ | |
xy1.x + Math.sin(angle * Math.PI / 180) * radius, | |
xy1.y + Math.cos(angle * Math.PI / 180) * radius | |
])) | |
], | |
{weight: 0.75, color: '#808080'} | |
).addTo(map) | |
); | |
}); | |
var anglelabels = [ | |
L.polyline([map.options.crs.unproject(left), map.options.crs.unproject(left)], {weight: 0.1, color: '#fffff', opacity:0}).addTo(map).bindTooltip('<b>270° </b>', {permanent: true, opacity: 0.7, direction: 'left'}).openTooltip(), | |
L.polyline([map.options.crs.unproject(right), map.options.crs.unproject(right)], {weight: 0.1, color: '#fffff', opacity:0}).addTo(map).bindTooltip('<b> 90°</b>', {permanent: true, opacity: 0.7, direction: 'right'}).openTooltip(), | |
L.polyline([map.options.crs.unproject(tops), map.options.crs.unproject(tops)], {weight: 0.1, color: '#fffff', opacity:0}).addTo(map).bindTooltip('<b>0°</b>', {permanent: true, opacity: 0.7, direction: 'top'}).openTooltip(), | |
L.polyline([map.options.crs.unproject(bottom), map.options.crs.unproject(bottom)], {weight: 0.1, color: '#fffff', opacity:0}).addTo(map).bindTooltip('<b>180°</b>', {permanent: true, opacity: 0.7, direction: 'bottom'}).openTooltip() | |
]; | |
// add a revolving "radar-hand" | |
var radar = L.geoJSON( | |
radarbeam, { | |
onEachFeature : function(feature, layer) { | |
var arclength = 2; | |
var sumangle = 360; | |
// sector is the slice of circle we'll use as a "beam shadow" | |
// aswell use it to test point-in-polygon for aircraft icon fade-out | |
var sector = { | |
type:"Polygon", | |
coordinates: [ [ | |
feature.coordinates[0], feature.coordinates[1], | |
feature.coordinates[1], feature.coordinates[0]]] | |
}; | |
var beamshadow = L.geoJSON( | |
sector, { | |
style: function(feature){ | |
return { | |
opacity:0.75, | |
color: '#109856', | |
weight:0.2, | |
className:'radar-hand' | |
} | |
} | |
}).addTo(map); | |
setInterval(function(){ | |
// animate "radar beam" | |
if (sumangle >= 360) { | |
sumangle = 0; | |
} else { | |
sumangle += arclength; | |
} | |
var beamlatlngs = layer.getLatLngs(), | |
beamshadowlatlngs = beamshadow.getLayers()[0].getLatLngs(); | |
// calculate a new location for the beam linestring. | |
beamlatlngs[1] = map.options.crs.unproject( | |
L.point([ | |
xy1.x + Math.sin(sumangle * Math.PI / 180) * radius, | |
xy1.y + Math.cos(sumangle * Math.PI / 180) * radius | |
]) | |
); | |
// and a new location for the trailing corner of the beam shadow | |
beamshadowlatlngs[0][1] = map.options.crs.unproject( | |
L.point([ | |
xy1.x + Math.sin((sumangle - 5 * arclength) * Math.PI / 180) * radius, | |
xy1.y + Math.cos((sumangle - 5 * arclength) * Math.PI / 180) * radius | |
]) | |
); | |
next = [beamshadowlatlngs[0][0], beamlatlngs[1], beamshadowlatlngs[0][1], beamshadowlatlngs[0][0]]; | |
beamshadow.getLayers()[0].setLatLngs(next); | |
layer.setLatLngs(beamlatlngs).bringToFront(); | |
realtime.getLayers().forEach(function(layer){ | |
var latlng = layer.getLatLng(), | |
el = layer.getElement(); | |
// test if the ircraft location is within the beam-shadow sector | |
if (realtime.options.pointInPolygon(latlng, next)) { | |
// remove fade-out if it exists | |
L.DomUtil.removeClass(el, 'end'); | |
// make icon visible | |
L.DomUtil.addClass(el, 'aeroplane-visible'); | |
// and set animation for tooltip | |
layer.openTooltip(); | |
setTimeout(function(){ | |
layer.closeTooltip(); | |
}, 5000); | |
} else { | |
// otherwise start fade-out | |
// this should kick in only if the classname is | |
// not already there | |
L.DomUtil.addClass(el, 'end'); | |
} | |
}); | |
}, 50); | |
}, | |
style: function(feature) { | |
return {color: '#109856', weight: 3, opacity:0.5} | |
} | |
}).addTo(map); | |
// Stamen's Toner basemap | |
L.tileLayer( | |
'https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', { | |
attribution: 'Map tiles by <a href="http://stamen.com">' + | |
'Stamen Design</a>, under' + | |
'<a href="http://creativecommons.org/licenses/by/3.0">' + | |
'CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">' + | |
'OpenStreetMap</a>, under' + | |
'<a href="http://www.openstreetmap.org/copyright">ODbL</a>.' | |
} | |
).addTo(map); | |
// and attribution | |
map.attributionControl.addAttribution( | |
'<br>Marker animation: <a href="https://github.com/perliedman/leaflet-realtime">Leaflet Realtime</a>' | |
); | |
map.attributionControl.addAttribution( | |
'<br>Air traffic location data from <a href="http://www.opensky-network.org">The OpenSky Network</a>\'s public <a href="https://opensky-network.org/apidoc/">API</a>' | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment