Implementing a real-time map of flying airplanes

Maps have nowadays become more and more important as a visualization tool, and real-time information often has a geographic component that can be displayed on the maps. In this recipe, we'll illustrate how we could present a flight map that shows us a set of airplanes and follow their position while they're flying to their destinations. What's interesting about this example is the fact that the coordinates of the displayed objects are constantly changing, and we have to update them on the map as quickly as possible. The other interesting thing is that time is a main component of the problem here. The information does not get into our system driven by any user's activity, but by the changes in time of the state of some external component.

From SignalR's perspective, this is a very simple application, and to develop it, we'll just need the following features:

  • Calling the methods on a hub from the client
  • Triggering client-side callbacks from the server

This sounds too basic, but that's exactly what's interesting about this sample, because building something as reactive as this with just the basics of SignalR makes us appreciate its power.

On the other hand, simulating the continuous change of state of observable things in an expressive way is less simple, but it's also something that's at the heart of many real-time problems. This is why we decided to spend some time on it in conjunction with SignalR.

Getting ready

Our application will consist of the following:

  • A Hub instance called FlightsHub, which will be used to notify the movement of the planes in real time and to subscribe to the specific planes in order to get notifications about their take-offs and landings.
  • A page for the client part, called index.html, with a Google Map on it.
  • A JavaScript file called flights.js that contains the client-side logic around the SignalR interaction and the manipulation of the map.
  • A styles sheet called flights.css.
  • Some more stuff to put things together (the Startup class, proper references, and so on).

In order to proceed, you'll have to build a new empty web application and call it Recipe49.

How to do it…

First of all, we add a reference to the Microsoft.AspNet.SignalR NuGet package to have everything that we need to run SignalR in our application. We'll also use a Google Map widget, which requires a personal API key that you'll have to create unless you do not already have one available. We will not spend any time on these details; in case you need more understanding on them, please refer to the online documentation at https://developers.google.com/maps/documentation/javascript/tutorial. Let's proceed with the following steps:

  1. We first add an index.html page to our project, and we put what we need on it, shown as follows:
    <!DOCTYPE html>
    <html>
    <head>
        <link href="Styles/flights.css" rel="stylesheet" />
        <script src="Scripts/jquery-2.1.0.js"></script>
        <script src="Scripts/jquery.signalR-2.0.2.js"></script>
        <script src="https://maps.googleapis.com/maps/api/js?
    key=YOUR_API_KEY&sensor=false"></script>
        <script src="/signalr/hubs"></script>
        <script src="Scripts/flight.js"></script>
    </head>
    <body>
        <div id="map"></div>
        <div id="messages">
            <button id="start" style="display: none">
                Start</button>
            <ul></ul>
        </div>
    </body>
    </html>

    As usual, we start with a reference to a CSS file, which, in this case, is named flight.css. We'll add it to the project later. Then, we reference the JavaScript client libraries that we always need for our SignalR applications whose versions will have to match the ones that we got from NuGet when we referenced the SignalR package.

    The new thing here is the reference to the Google Maps JavaScript API; for more details about its usage, please refer to its documentation. Please pay attention to the key parameter in the URL; in your code, you'll have to replace the YOUR_API_KEY placeholder with a valid key that you are entitled to use.

    Finally, we add the reference to the dynamic hubs endpoint (/signalr/hubs) and a reference to the flights.js file that we'll create shortly to host our application logic.

    The content of the page's body is very simple; it consists of just a couple of div elements to host the surface of the map, a button to start the flights, and an unordered list to display the live notifications that we'll receive.

  2. Let's add some styling directives to get things to behave properly on the page by adding the flights.css file that we mentioned earlier inside a folder called Styles. The following will be its content:
    html {
        height: 100%;
    }
    
    body {
        height: 100%; margin: 0; padding: 0;
    }
    
    #map {
        height: 100%; z-index: 1;
    }
    
    #messages {
        z-index: 10; position: absolute;
        top: 0; right: 0; width: 400px; 
        text-align: right; 
        margin-top: 20px; margin-right: 20px;
        padding: 10px;
        font-family: "Helvetica";
    }
    #messages ul {
        list-style-type: none;
    }
    
    li {
        width: 100%; position: absolute;
        background-color: yellow; opacity: 0.5;
        margin: 3px;  padding: 2px;
    }
    li.subscribed {
        background-color: lightgreen;
    }

    Nothing special here; we are just placing a right-hand side notifications bar on top of our fullscreen map. Please note just the height:100% directives on the html, body, and #map selectors; these are necessary to correctly display our Google Map widget fullscreen.

Let's move to the server side of the application. As already mentioned, we need a way to generate real-time information about how planes are moving, and we need to push that information to the connected clients, but first, we need to have some information about the actual planes that we are managing, such as their code and some details about their routes. Ideally, that would be the responsibility of an external system, and we have already been illustrating how to properly hook an external service inside a hub and how to have such a service refer to a hub's context. For this recipe, we'll keep things very basic and we'll have everything inside our hub; we leave the task of decoupling these aspects as an exercise for the reader. Let's proceed with the following steps:

  1. We first create the bootstrapping Startup class using the corresponding Visual Studio template that will add OwinStartupAttribute, and we'll add to it the standard Configuration method's body:
            public void Configuration(IAppBuilder app)
            {
                app.MapSignalR();
            }
  2. Then, we move on to the FlightsHub class for the next few steps. We first create it and change its code, as shown:
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Drawing;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace Recipe49
    {
        [HubName("flights")]
        public class FlightsHub : Hub
        {
            ...
        }
    }
  3. As we said, we'll be managing everything from inside FlightsHub, including the generation of real-time information about the flights. To make things more easily observable, we'll have a specific method, named Go(), to make all the flights take off. We'll just want to guarantee that this specific method can have an effect just once, so we'll take care of that with some static fields:
            private static bool Flying;
            private static readonly object Locker =
                new object();
    
            public void Go()
            {
                if (Flying) return;
    
                lock (Locker)
                {
                    if (Flying) return;
    
                    Flying = true;
    
                    ...
                }
            }

    We use the Flying static Boolean field to mark when the flights have actually started and another static field called Locker to allow the operation of starting the flights to just the first call on it. This code is pretty basic and not scalable, but it's good enough for our purposes.

  4. Let's now gradually fill the rest of the Go() method. We start by adding the following geographic context to our sample:
    var towns = new Dictionary<string, PointF>
    {
        {"Turin",     new PointF( 45.2008018F,  7.6496301F)},
        {"Berlin",    new PointF( 52.3800011F, 13.5225F)},
        {"Madrid",    new PointF( 40.4936F,    -3.56676F)},
        {"New York",  new PointF( 40.639801F, -73.7789002F)},
        {"Istanbul",  new PointF( 40.9768982F, 28.8146F)},
        {"Paris",     new PointF( 49.0127983F,  2.55F)},
        {"Cape Town", new PointF(-33.9648018F, 18.6016998F)},
    };

    These are the coordinates of the towns that our planes will be flying to or from.

  5. Then, let's define our flights, shown as follows:
    var flights = new[]
    {
        new
        {
            Code   = "LH100",   Color  = "red",
            From   = "Turin",   To     = "Berlin",
            Speed  = 10,        Period = 500,
        },
        new
        {
            Code   = "AZ150",   Color  = "blue",
            From   = "Turin",   To     = "Madrid",
            Speed  = 8,         Period = 1500,
        },
        new
        {
            Code   = "XX150",   Color  = "green",
            From   = "New York",To     = "Istanbul",
            Speed  = 30,        Period = 400,
        },
        new
        {
            Code   = "AA777",   Color  = "orange",
            From   = "Paris",   To     = "Cape Town",
            Speed  = 35,        Period = 2000,
        }
    };

    Each of the flights has a code, a color to be used on the map for the corresponding marker, outbound and inbound towns, a speed value (the custom unit of measure that we use for speed here is latitude degrees per hour), and a transponder period that indicates the number of milliseconds between each signal that comes from the plane to notify us of its current position. Hence, each plane will have a different speed and will push the information at a different rate.

  6. To make events on the map easier to observe, we accelerate the time using a correction factor:
    const int simulationFactor = 20; //1 hours = X seconds

    This number will determine how many seconds will be used to represent an actual hour of time; this way, we can compress time and see things happening faster. We set up a value of 20, which will make one hour pass in just 20 seconds (!).

Now, we get to the complex part: how to make things happen over time. We could have applied a few different approaches, but we decided to use the Rx framework, which we can download from NuGet by adding the Rx-Main package. Rx works around the duality between the concepts of enumerable and observable, and makes working with IObservable as easy as using IEnumerable the way we do with Linq. It also adds a lot of infrastructure to compose observable sequences and to add scheduling features to the picture. This is really like adding time as a first-citizen component of our streams, making them available to the Linq environment as sequences. The only noticeable difference is that, in this case, the sequences are pushing information towards us instead of being pulled by us, as it happens with IEnumerable. This is why Rx and SignalR are technologies that go along well; both of them are about pushing information and therefore, they can be used together in very effective ways.

With Rx in place and the addition of the using System.Reactive.Linq; directive, we can start writing the generation of our flights' streams of data, which will actually fit in a single Linq query expression! Our goal is to enumerate every flight that we defined earlier to make it generate information about what is its position at specific points in time.

Each plane will move repeatedly back and forth between the towns specified with the From and To properties, flying at the indicated speed and taking a break of a constant duration between each leg; our expression will have to express it correctly. Let's proceed by performing the following steps:

  1. Let's start the declaration of our query expression using the following code:
    var signals =
        from flight in flights.ToObservable()

    The first necessary step is to use the ToObservable() extension method from Rx to take our collection of flights and push it into the realm of observables where it will be composed with other push streams.

  2. For each flight, we calculate some geometric information:
        let f        = towns[flight.From]
        let t        = towns[flight.To]
        let dx       = t.X - f.X
        let dy       = t.Y - f.Y
        let d        = Math.Sqrt(
                           Math.Pow(dx, 2) + Math.Pow(dy, 2))

    Here, we are using the From and To properties to get the coordinates of the two points each flight will be flying across, and we use them to calculate the absolute distance and its latitudinal and longitudinal components, expressed in latitude degrees.

    Note

    This is not how real planes fly. We are treating the problem as if the Earth was flat, but it's not, and we shouldn't use the Pythagoras theorem to calculate the shortest path. Nevertheless, this approximation will work fine for our goals, and it will allow us to concentrate on aspects that are more relevant to this book. At the same time, the Google Map widget uses a flattening projection of the Earth, so in our example, the planes will be traveling along straight lines. This, again, is not how a real flight would look like on a projective map, but that's not an issue for us.

  3. Let's start doing some time-related computations, shown as follows:
        let time     = d / flight.Speed
        let sampling = (double)1000 / flight.Period
        let steps    = (int)(sampling*simulationFactor*time) 
                       + 1
        let sx       = dx / steps
        let sy       = dy / steps

    We first calculate the time required for each flight to complete, then we calculate the frequency of the transponder (normalized to samples per seconds = Hertz) that we use to determine how many times a plane would notify its position for each leg that it runs (that's steps, and we add 1 to the total to take both the starting and destination points into account). The simulationFactor constant is used to correct the number of steps. Finally, we measure how much space will separate each transponder signal on both the longitudinal and latitudinal components.

  4. We also want to know how many times the transponder will send signals while the plane is down on earth waiting to take off again:
        let paused   = (int)(0.5 * simulationFactor * sampling)

    For simplicity, we make each stop half an hour long, and therefore, the number of signals is easily computed, including the correction factor.

  5. We are ready to define the actual trajectory of each flight, starting with the outbound leg, shown as follows:
        let outbound = from i in Enumerable.Range(0, steps)
                       select new
                       {
                           Index   = i,
                           Landed  = false, 
                           Touched = i == steps-1,
                           Town    = flight.To
                       }

    It's pretty simple, we generate as many samples as the number of steps that we calculated earlier, and for each of them, we indicate the following:

    • The index of the step, which also indicates its current position on the straight line on which it's running (if we imagine to divide that line by the number of steps)
    • Whether the plane has landed
    • Whether it's the last sample
    • Its destination
  6. We do something similar to generate the information to push when the plane is waiting to take off again, shown as follows:
        let delay1   = from i in Enumerable.Repeat(steps, 
                           paused)
                       select new
                       {
                           Index   = i,
                           Landed  = true,
                           Touched = false,
                           Town    = flight.To
                        }

    Here, we use the paused expression to determine how many signals to send while waiting. For this sequence, the Index value will always be equal to the steps value, which indicates that for all the time we're waiting, we're at the end of the calculated trajectory.

  7. We repeat the last two points for the inbound leg, shown as follows:
        let inbound  = from i in Enumerable.Range(0, steps)
                       select new
                       {
                           Index   = steps - i,
                           Landed  = false,
                           Touched = i == steps - 1,
                           Town    = flight.From
                       }
        let delay2   = from i in Enumerable.Repeat(0, paused)
                       select new
                       {
                           Index   = i,
                           Landed  = true,
                           Touched = false,
                           Town    = flight.From
                       }

    Notice the Index value. For the inbound sequence, this is decreasing to go back to the starting point, and for the delay2 sequence, it is always 0 to indicate that the signals are coming out from the place where the flight first took off.

  8. Now that we have the four components of a whole flight, we just concatenate them in a single sequence, from which we then generate an array because later we'll access its components by its position, as shown in the following code snippet:
        let route    = outbound.Concat(delay1)
                               .Concat(inbound)
                               .Concat(delay2).ToArray()
  9. We're now ready to introduce the time component, and for that, we just join our flights and the related sequences we just produced with a sequence generator from Rx, shown as follows:
        from st in Observable.Interval(
            TimeSpan.FromMilliseconds(flight.Period))

    Observable.Interval generates an infinite stream whose values are pushed down to the consumers at a defined frequency in time. In our case, of course, this will be the period of the transponder for each plane. Whoever will subscribe to this sequence will actually be free from any activity until Observable.Interval will wake it up with a new signal. How things are actually implemented behind the scenes to introduce this efficient, concurrent, and asynchronous behavior is out of scope here, but it's definitely one of the main reasons you should put Rx in your tool belt.

  10. From now on, it's as if the rest of our expression will be executed at regular intervals; we just need to link the periodic samples to the information about the actual position of the plane in order to produce what we need to push to the clients, as shown in the following code snippet:
        let w        = route[st%route.Length]
        let s        = w.Index
        let x        = f.X + sx * s
        let y        = f.Y + sy * s
        let landed   = w.Landed
        select new
        {
            flight.Code,
            w.Touched,
            w.Town,
            Landed   = landed,
            X        = x,
            Y        = y,
            Color    = landed 
                     ? "black" 
                     : flight.Color,
        };

    We first normalize our infinite stream of indices (st) towards the actual length of each route, and then everything becomes quite straightforward, with a final select statement producing all the necessary details that we need to send to the clients for each sample. A landed plane will be shown in black. Otherwise, the color specified with its definition will be used.

  11. We're almost done; we just need a single consumer to actually make the generation of signals happen, and the way we built the Go() method will guarantee us that only one subscriber will be in place, shown as follows:
    signals.Subscribe(
        flight =>
        {
            Clients.All.sample(flight.Code, 
                flight.X, flight.Y, 
                flight.Color, flight.Landed);
            if (!flight.Touched) return;
    
            Trace.WriteLine(
                string.Format("Notifying {0}...", 
                    flight.Code));
            Clients.Group(flight.Code).touched(flight.Code,
                flight.Town, 
                DateTime.Now.ToLongTimeString());
        });

    The Subscribe() method allows us to define a consumer of signals. Each time one of the signals will be pushed to the subscriber, all of its details will be collected and notified to the connected clients by calling the sample callback on Clients.All dynamically. If the plane has landed, we'll trigger a further notification using the touched callback, targeting anybody who has subscribed to a group called after the flight's code.

    Please observe that the lambda expression used to define the subscriber will be fully executed at each sample; this means that the Clients members will be processed each time; therefore, both sets of the currently connected clients and the connections that belong to the previously mentioned group will always be up to date; this way, late connections will catch up with the current positions on the first signal that is received.

  12. Before leaving the Go() method, we eventually notify everybody that all the planes have started flying, as shown in the following line of code:
        Clients.All.started(Flights);

    The Go() method is now complete, and with that, the generation of the live stream of data about the flights is complete as well, although a hub's method wasn't strictly necessary in this case. More suited to a hub method is the task of exposing to the users a way to register their interest in receiving notifications about the state of a single airplane.

  13. Let's add a Notify() method to the hub to ask for notifications:
    public Task Notify(string code)
    {
        Trace.WriteLine(
            string.Format("Asking to notify {0}", code));
        return Groups.Add(Context.ConnectionId, code);
    }

    The implementation uses the code of the flight to subscribe the caller to a group that is named after the flight. This code matches what we saw earlier in the Subscribe call to notify these subscribers.

  14. We complete FlightsHub with an OnConnected handler, which will tell every new client whether the flights are flying already or not:
    public override Task OnConnected()
    {
        Clients.All.started(Flying);
    
        return base.OnConnected();
    }

We can now move to the client-side code located in the flights.js file. We need to perform some manipulations on the map to react on clicks on the markers that represent the planes, in order to subscribe for notifications from them. Of course, we'll also need to set up the SignalR callbacks and start the connection. The following steps show us the code, with just a few brief comments, because most of it is plumbing code that is not strictly related to SignalR:

  1. We start by defining some useful variables and functions, shown as follows:
    $(function() {
    
        var hubs = $.connection.hub,
            flights = $.connection.flights,
            markers = {},
            subscribed = {},
            map = new google.maps.Map($("#map")[0], {
                center: new google.maps.LatLng(45, 8),
                zoom: 2
            }),
            buildIcon = function(color) {
                return {
                    path: google.maps.SymbolPath.CIRCLE,
                    scale: 8,
                    strokeColor: color
                };
            };
    
        ...
    
    });
  2. We then define SignalR's callbacks, starting from sample. Here, we add code to perform the following steps:
    • Add a marker for each plane the first time a signal arrives from it.
    • Move the marker at every subsequent signal.
    • Attack a click handler on each marker to subscribe to the notifications from it.

    The following is a code snippet that shows us all this:

        flights.client.sample = function (
            code, x, y, color, landed) {
            if (!markers[code]) {
                markers[code] = new google.maps.Marker({
                    icon: buildIcon(color),
                    map: map,
                    title: code
                });
                google.maps.event.addListener(
                    markers[code], 'click',
                    function () {
                        var li = '<li class="subscribed" />';
                        if (!subscribed[code]) {
                            flights.server
                                .notify(code)
                                .done(function () {
                                    var $li = $(li)
                                        .text('Watching ' + 
                                            code);
                                    $('#messages ul')
                                        .prepend($li);
                                    subscribed[code] = true;
                                });
                        }
                    });
            } else if (markers[code].landed !== landed) {
                markers[code].setIcon(buildIcon(color));
                markers[code].landed = landed;
            }
            markers[code].setPosition(
                new google.maps.LatLng(x, y));
        };
  3. The next callback is touched, as shown in the following code snippet, which is triggered each time a plane touches land, and reaches each client who subscribed to that particular plane. This is used to fill the list of notifications about each plane the user has subscribed to:
        flights.client.touched = function (code, town, time) {
            var m = code + ' landed in ' + town + ' @ ' + time;
            $('#messages ul').prepend($('<li/>').text(m));
        };
  4. The last callback to implement is started, as shown in the following code snippet, which is received by each client when it first connects and when the flights start flying. We use it to decide whether the Start button should be displayed or not:
        flights.client.started = function (flying) {
            $('#start').toggle(!flying);
        };
  5. Finally, we use the usual code to start the connection:
        hubs.start().done(function () {
            $('#start').click(function () {
                flights.server.go();
            });
        });

    When the connection is ready, we bind the Start button to a click event handler, which will ask FlightsHub to start the generation of the signals from the flights.

We're ready to test our application. We build the project, launch the application, and navigate to the index.html page, which will display a map of the world and a Start button to make the flights move. As soon as we click on it, a few circular markers will start moving on the map, each one representing a different plane. We'll see how they change color according to the state that a flight flies over, and we'll be able to click on them to receive notifications whenever they touch land. We can also open multiple tabs pointing at the same address and appreciate how all of them are updated in real time.

How to do it…

The code of this recipe was pretty complex; however, the SignalR features that we've been using were basic ones, which clearly demonstrate that, with SignalR, real-time messaging features are not the most complex part of our application anymore.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset