/posts  ·   /talks  ·   /boat  ·   /#30DayMapChallenge 2020 | 2022  ·  /About

Building a flightradar in Leaflet (part I)

This post is about building a radar-like map in Leaflet with data from the OpenSky Network’s public API.

It’s a quick one-evening hack so don’t expect much finesse. The main motivation behind it being just to prove myself that I can do it.

API

The OpenSky Network provides a public API for retrieving live airspace information for research and non-commercial purposes. The API documentation is available here. Instead of using it directly I set up a proxy which in turn formats the response data to GeoJSON. The proxy itself is fairly simple and written in Python. Use requests to retrieve the data and then reformat everything to GeoJSON like this:

import requests
from collections import OrderedDict

# note the use of bounding box coords in the URL
# this is roughly the area for Latvia, Estonia and
# a small part of southern Finland
url = 'https://opensky-network.org/api/states/all?lamin=57.48&lomin=21.6&lamax=59.82&lomax=28.52'
keys = [
    'icao24', 'callsign', 'origin_country', 'time_position', 'last_contact',
    'longitude', 'latitude', 'baro_altitude', 'on_ground', 'velocity',
    'true_track', 'vertical_rate', 'sensors', 'geo_altitude', 'squawk',
    'spi', 'position_source'
]

def get_flight_radar_data():
    r = requests.get(url)
    r.raise_for_status()
    return to_geojson(r.json())

def to_geojson(data):
    f = [
        OrderedDict(
            type='Feature',
            id=ac[0],
            geometry=OrderedDict(type='Point', coordinates=[ac[5],ac[6]]),
            properties=OrderedDict(zip(keys, ac))
        ) for ac in data.get('states', [])
    ]

    return dict(
        type='FeatureCollection',
        features=f
    )

Saved this as proxy.py and then used my existing Django install on a shared hosting site to run it. Otherwise it can be run on localhost as a Flask app with

import json
from flask import Flask
from flask import Response

from proxy import get_flight_radar_data

app = Flask(__name__)

@app.route('/flightradar')
def flightradar():
    return Response(
        response=json.dumps(get_flight_radar_data()),
        status=200,
        mimetype='application/json'
    )

Save this as flaskapp.py in the same path as the previous flightradar.py, and then fire it up:

$ export FLASK_APP=flaskapp.py
$ flask run
 * Serving Flask app "flaskapp.py"
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

So now the GeoJSON enabled API is ready for use and the data can be viewed using other applications (e.g. QGIS) aswell.

Animating (realtime) markers on a Leaflet map

There’s the excellent Leaflet.Realtime library for putting realtime data on a Leaflet map. This will help us to query the API we just set up after a preset interval and will manage all the necessary redraws etc.

Set up Leaflet map

Let’s start off by setting up the Leaflet map using the Stamen Toner basemap. In your favourite text-editor:

<!DOCTYPE html>
<html>
    <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>
        <style>
            #map {
                position: absolute;
                top: 0;
                left: 0;
                bottom: 0;
                right: 0;
            }
        </style>
    </head>
    <body>
        <div id="map">
        </div>
        <script>
            // set up map
            var center = [58.65, 25.06];
            var map = L.map('map').setView(center, 7);

            // 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);
        </script>
    </body>
</html>

Save this as flightradar.html and open the file in your web-browser.

Animate the data

Now in order to use Leaflet.Realtime to do the data querying and draw/animation, add

<script
    src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-realtime/2.0.0/leaflet-realtime.min.js"
    type="text/javascript">
</script>

to the head section of your flightradar.html file. Next work a bit on the body’s script tag and add

// air traffic locations layer,
// should actually request when the "radar beam" is pointing N e.g
// but we'll go with L.Realtime for the moment.
var realtime = L.realtime('http://127.0.0.1:5000/flightradar', {
    // interval of data refresh (in milliseconds)
    interval: 10 * 1000,
    getFeatureId: function(feature) {
        // required for L.Realtime to track which feature is which
        // over consecutive data requests.
        return feature.id;
    },
    pointToLayer: function(feature, latlng) {
        // style the aeroplane loction markers with L.DivIcons
        var marker = L.marker(latlng, {
            icon: L.divIcon({
                className:'aeroplane-visible',
                iconSize: [10,10]
            }),
            riseOnHover: true
        }).bindTooltip(
            // and as we're already here, bind a tooltip based on feature
            // property values
            '<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;
    }
}).addTo(map);

We’ll still need to define the looks of the aeroplane markers. As this map will be in the style of a simple radar, then a let’s have it a square with a greenish color. Remember we added the markers in the pointToLayer function with an extra className value of aeroplane-visible so simply define

.aeroplane-visible {
    background: #109856;
    border: none;
    opacity: 1.0;
}

in the style of the html file.

Don’t forget to add the required attribution aswell (this goes in the script tag of html body)

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>'
);

Save the file and reload it in the web browser and behold! There’s a bunch of green squares moving around every n seconds that pass.

All the mouseover events with tooltips opening and closing are done automatically so this will give more time and space to play around with other options. For example switch from using the divIcon-styled markers to one of the aeroplanes from Font Awesome (see Leaflet.awesome-markers plugin on how to do that), maybe scale the icons according to some feature’s properties and rotate them according to the direction travel.

But anyway, instead what I wanted was to have a greenish radar hand moving around with the aeroplane markers slowly fading out as the hand has passed over them something in the line of:

https://giphy.com/gifs/radar-ki1rmMhjvLlm via GIPHY

For that we’ll need to set up a polar graticule and a 360-degrees revolving line, and find a way to animate the aeroplane markers in order to simulate the look of a real radar. But I’ll discuss that in the next writeup. The full working code for flightradar.html as well as proxy.py and flaskapp.py can be found in this gist.

Happy exploring! :)