

Goal
In this tutorial, we are going to build a simple dashboard that shows some airplanes in the air in real-time thanks to the open API provided by ADS-B. We will use some of the modules and explain their usage and how to adjust them to our needs. So let's not beat around the bush and jump in to make our first dashboard!Step 1: An empty map
index.html
We start by creating theindex.html
that will contain all code for our dashboard:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>flight stat</title>
</head>
<body>
<script></script>
</body>
</html>
We already added the script
tag that will be populated with all the code.
loading libraries
First of all, we add all dependencies in the head ofindex.html
:
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson@3.0.2/dist/topojson.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.4/lodash.min.js"></script>
The libraries can be obtained through the links listed in the home of the documentation.
In order to add the map, we first include the corresponding module, along with the
widget
module that is required by all other widgets.
So, again in the head, after the dependencies goes the following
<script src="build//widgets.min.js"></script>
<script src="build//adt/widgets.map.min.js"></script>
In this tutorial, the modules are available from the documentation, but you need to change the above to fit your
directory structure.
And that was all the HTML we had to write, from now on we will only write javascript code to add the all widgets,
fetch the data from the flight status API and add interactions within the dashboard.
adding the map
Now we have everything to build our very first dashboard: a static map with no data shown at all. Just add ascript
tag in the body
and create a map widget that fills the entire window:
<script>
var map = new adt.widgets.Map("flight")
.x(0) // Top-left corner is adjusted to
.y(0) // the window
.width(window.innerWidth) // Size is set to the window size
.height(window.innerHeight)
.render(); // Must be called to draw the map
</script>
And that's it, congratulations, you just built your first map dashboard in no more than 22 lines!
This is how it should look like.
Although we don't have any data yet and it lacks some styling, our map can be zoomed with scroll or by clicking on
any country!
Even more awesome is that we are already through the hard part, adding libraries and setting up a first working
dashboard.
In the following steps we only add some designing and attach data to the map so that we have a fully functioning
application.
chaining
All widgets have been designed with the chaining rule in mind that enables us setting multiple properties of the widget in a simple and clear manner. As you can see, once we created the map widget, we just set the parameters by adding consecutive function calls after the constructor. This is a general rule: all widget properties can be appended in the chain, as the (vast majority of) methods return a reference to the widget itself. When this is not the case, it is noted in the documentation.Another important behavior is the method
render()
that must be called in order to draw the
widget.
Under the hood, adt.widget
simply collects the various parameters of the widget while keeping it hidden
and when we call render()
, the widget actually becomes visible.
Moreover, render()
is implemented according to the get-or-create design, that is, once it is called,
further calls simply update the widget with the new style and data.
Therefore, you can simply modify the widget on-the-run and then render it in the end.
styling and basic interactions
This is all nice and good but we want to have some unique dashboard that we can play with! Let's start with the look and feel:adt.widgets.Map
has some convenience functions we can call to set
up some basic colors.
For the sake of simplicity, we just append these right before rendering the map:
<script>
var map = new adt.widgets.Map("flight")
.x(0) // Top-left corner is adjusted to
.y(0) // the window
.width(window.innerWidth) // Size is set to the window size
.height(window.innerHeight)
.backgroundColor("#111")
.borderColor("#888")
.foregroundColor("#222")
.render(); // Must be called to draw the map
</script>
The functions backgroundColor(color)
, foregroundColor(color)
and borderColor(color)
are rather talkative, so no further explanation is needed.
If you take a look at the map now (here is a live example), you can see that countries are
highlighted automatically, due to the little brighter foreground color.
Also, if you zoom in on a country, it remains highlighted.
Great, now that we have our map, let us fetch some data from the flight stats API and show it!
Step 2: Adding data
fetching data
Here we make use of theadt.rest
module that implements the REST
class, which is simply
a wrapper for the existing request methods (more specifically d3
methods).
First, add
<script src="build/widgets.min.js"></script>
to the head of index.html
.
To get the data, we create a new REST
object that is directed to the endpoint of the API:
// We use the cors-anywhere app to access the API
// (even from localhost)
var proxy = "https://cors-anywhere.herokuapp.com/";
// Create REST object that fetches the data
var api = new adt.rest.Rest(
proxy + "https://public-api.adsbexchange.com/VirtualRadar/AircraftList.json"
);
The above snippet creates a REst object pointed at the flight stats base URL (with a proxy to avoid CORS errors).
Sometimes the Heroku app used for the CORS proxy is down and the database is not accessible.
If this happens, please try to refresh the page a few minutes later (or check
https://cors-anywhere.herokuapp.com if the app is
running again).
To query the data from the database, we simply call the REST.json
method that creates the HTTP request
URL and fetches the data that is expected in a JSON format:
// Fetch the data
api.json({fCallS: "SAS"}, function(data) {
// Data is fetched successfully
updateMap(data);
}, function(error) {
// Print error to the console
console.log(error);
});
The first argument of REST.json
is the parameters of the query, which in this case is a filter in the
ADS-B database to have only flights with a flight call starting with 'SAS'.
The second and third arguments are callbacks that will be triggered in case of success or failure.
draw on the map
What is left is to implementupdateMap
that will parse the data from the JSON response and add it to
the map.
But before that, we add a static and a dynamic drawing layer to the map:
map.staticLayer.add("SAS"); // Add static layer 'SAS'
map.dynamicLayer.add("SAS"); // Add dynamic layer 'SAS'
For the sake of simplicity, we add each flight as a white dot.
The static dot will show the trail of the flight, the dynamic dot will tell if it is updated.
After a little investigation of the structure of the data we receive from ADS-B, we can extract the coordinates of
each aircraft:
// Update method
function updateMap(data) {
// Loop through aircrafts
data.acList.forEach(function(d) {
// Get geo coordinates
var latLon = [d.Lat, d.Long];
// Add to static map as trail of the flight
map.staticLayer.draw.dot("SAS", latLon, 1, "white");
// Add to dynamic map to see the plane is updated
map.dynamicLayer.draw.dot("SAS", latLon, 10, "white", 400);
});
}
Now if you open the page, you should see some while dots around Scandinavia after the flashing animation of circles
(this is how it should look).
The animations are shown on the dynamic layer with initial size of 10 pixels, and a duration of 400ms, whereas the
static dots are added to the static network.
To speed up things a little, the map uses canvas
for the static drawings and svg
for
the animated ones.
Note that the map only draws elements with valid coordinates.
If we really wanted, we could check if the dot was drawn by examining the return value of the methods
draw.dot
in each layer, but for now we skip that.
some improvements
We can make our map a little more active by wrapping the complete data fetching and draw in asetInterval
:
// Query the database every 10 seconds
setInterval(function() {
api.json({fCallS: "SAS"}, function (json) {
// Data is fetched successfully
updateMap(json);
}, function (error) {
// Print error to the console
console.log(error);
}, false);
}, 10000);
Since we don't clear the static page, we can actually see the trail of the flights, which become more revealing if
you zoom in on the map around Scandinavia.
To spice things up a little, let us track each flight individually by adding a variable that contains the flight
info and the updateMap
function, create a new flight whenever we see an unknown call.
We also set a unique color for each new flight so that we can distinguish among them.
When new data is available, we only update those flights that have a displacement more than 10 degrees in total
along the latitude/longitude.
All together our new updateMap
method:
// Update method
var flights = {};
function updateMap(data) {
// Loop through aircrafts
data.acList.forEach(function(d) {
var id = d.Call;
if (!flights.hasOwnProperty(id))
flights[id] = {
color: "rgb(" + parseInt(255*Math.random())
+ "," + parseInt(255*Math.random())
+ "," + parseInt(255*Math.random()) + ")"
};
// Get geo coordinates
var latLon = [d.Lat, d.Long];
// Show airplane if it is new or latitude/longitude has an angle difference of 0.1
if (!flights[id].hasOwnProperty(latLon) ||
Math.abs(flights[id].latLon[0]-latLon[0])+Math.abs(flights[id].latLon[1]-latLon[1]) > 10) {
// Add to static map as trail of the flight
map.staticLayer.draw.dot("SAS", latLon, 1, flights[id].color);
// Add to dynamic map to see the plane is updated
map.dynamicLayer.draw.dot("SAS", latLon, 2, flights[id].color, 400);
}
// Update coordinates
flights[id].latLon = latLon;
});
}
For a reference, here you can see a working version of the automated and colored map.