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:
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.
Our application will consist of the following:
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.index.html
, with a Google Map on it.flights.js
that contains the client-side logic around the SignalR interaction and the manipulation of the map.flights.css
.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
.
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:
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.
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:
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(); }
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 { ... } }
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.
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.
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.
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:
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.
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.
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.
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.
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.
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:
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.
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.
let route = outbound.Concat(delay1) .Concat(inbound) .Concat(delay2).ToArray()
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.
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.
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.
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.
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.
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:
$(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 }; }; ... });
sample
. Here, we add code to perform the following steps: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)); };
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)); };
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); };
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.
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.