Skip to content

Instantly share code, notes, and snippets.

@tkardi
Last active April 11, 2023 08:09
Show Gist options
  • Save tkardi/99fb0a96b12068157af30cf806651bcf to your computer and use it in GitHub Desktop.
Save tkardi/99fb0a96b12068157af30cf806651bcf to your computer and use it in GitHub Desktop.
Code companion to 'Building a flightradar in Leaflet (pt II)'
<!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