Implementing a "pets finder" application

The previous recipe showed us how a map can be used to display live information, updated at a relatively high frequency, in a scenario where the user is passive in practice and just observes what a server is broadcasting. In this recipe, we want to explore a much more interactive scenario, where the information displayed on the map is provided by the users.

This application can be used to place the name and a picture of a pet that has been lost on a map. These details will be added to other users' maps in real time. Anybody observing the map could drag-and-drop any marker on a different position to notify that the pet was there. Finally, the user who first raised an alarm about a specific lost pet can declare that he/she found it, and this action will correspond to the removal of the markers related to that specific animal from all the connected maps. This is illustrated in the following screenshot:

Implementing a "pets finder" application

From a SignalR's perspective, we'll use the following features:

  • Calling methods on a hub from the client
  • Triggering client-side callbacks from the server
  • Dependency injection
  • Custom authorization workflow
  • Services' extensibility (replacing a default service)
  • Custom query string parameters

This example is quite interesting because it puts together a consistent amount of features that we've been analyzing throughout the book.

Getting ready

Our application consists of the following:

  • A Hub called PetsFinderHub, which will be used to notify about lost pets' sightings in real time and to subscribe to specific pets in order to get notifications about their movements.
  • A custom service defined by the IPetsFinder interface, with a proper implementation and a couple of supporting types. This will be the core of the application, with the responsibility of storing information about the pets and supplying it back to PetsFinderHub when required.
  • An authorization attribute called AuthorizeLoggedAttribute.
  • A custom implementation of IUserIdProvider.
  • A page for the client part, called index.html, with a Google Map on it.
  • A JavaScript file called pets.js that contains the client-side logic around the interactions with SignalR and the Google Map, plus some extra bits to manipulate images.
  • A style sheet called pets.css.
  • Some more stuff to put things together (the Startup class to set up dependencies and start SignalR, proper server and client references, and so on).

In order to proceed, you'll have to build a new empty web application and call it Recipe50. Before proceeding, you should be aware of the fact that this sample uses the new HTML5 FileReader API to read the content of an image file that is dropped onto a div element. Only modern browsers support it, and therefore, this sample cannot work on older browsers. Please check http://caniuse.com/filereader to check whether your browser will support this sample.

How to do it…

First of all, we reference 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 that requires a personal API key; for more details about it, please refer to the previous recipe. Let's proceed with the following steps:

  1. We first add an index.html page to our project, with the following content:
    <!DOCTYPE html>
    <html>
    <head>
        <title></title>
        <link type="text/css" 
            href="/Styles/pets.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/pets.js"></script>
    </head>
        <body>
            <div id="map"></div>
            <div id="panel">
                <div id="login-panel">
                    <label>Nickname:</label>
                    <input type="text" id="user"/>
                    <button id="login">Log in</button>
                </div>
                <div id="logged-panel">
                    <label>Nickname:</label>
                    <span id="nickname"></span>
                </div>
                <div id="lost-panel">
                    <div id="lost-panel-name">
                        <h5>Lost your pet?? 
                            What's its name?</h5>
                        <label>Name:</label>
                        <input type="text" id="name" />
                        <button id="proceed">Proceed</button>
                    </div>
                    <div id="lost-panel-photo">
                        <h5>Drop its photo here...</h5>
                        <div id="photo"></div>
                    </div>
                    <div id="lost-panel-location">
                        <h5>...and locate it on the map</h5>
                        <p>
                            Position your map in the right 
                            area, when ready click on the 
                            'Locate it!' button and then click 
                            on the right position on the map.
                        </p>
                        <button id="lost">Locate it!</button>
                    </div>
                </div>
                <button id="found">Found!</button>
                <ul id="messages"></ul>
            </div>
        </body>
    </html>

    The page starts with the usual references to JavaScript libraries, plus a reference to the Google Maps endpoint and to a file called pets.js, which we'll add to the project shortly. The body section of the page contains the div element for the map and a series of panels that will be displayed or hidden according to the specific actions that the user will perform on the application. It's pretty long, but there is nothing worth any specific comment.

  2. The page is styled using a file called pets.css, placed in the Styles folder:
    html {
        height: 100%;
    }
    body {
        height: 100%; margin: 0; padding: 0;
    }
    #map {
        height: 100%; z-index: 1;
    }
    
    #panel {
        z-index: 10; position: absolute;
        top: 0; right: 0; width: 300px; 
        margin: 30px; padding: 10px;
        font-family: "Helvetica";
        background-color: lightgreen; opacity: 0.7;
    }
    #messages {
        list-style-type: none;
    }
    #messages li {
        background-color: yellow;
        width: 100%; margin: 3px; padding: 2px;
    }
    #photo {
        width: 240px; height: 240px; margin: 3px;
        background-color: red; border: 5px dotted lightgrey;
        z-index: 2;
    }
    #lost-panel, #logged-panel, #lost-panel-photo,
    #lost-panel-location, #found {
        display: none;
    }

Enough with markup and styling; let's move on to see how the server-side portion of the application is actually designed.

  1. Our code will be built around the concepts of pet and location. We add a file called Pets.cs to the project, where we'll add all the types involved in the implementation of the business rules of the application. Inside Pets.cs, we first add the Location and Pet types, which are straightforward and do not need any particular explanation:
        public class Location
        {
            public Location(DateTime when, PointF where)
            {
                When = when;
                Where = where;
            }
            public PointF Where { get; private set; }
            public DateTime When { get; private set; }
        }
    
        public class Pet
        {
            private readonly ICollection<Location> _locations;
    
            public Pet(
                string user, string name, 
                string photo, DateTime when)
            {
                User = user;
                Name = name;
                Photo = photo;
                When = when;
                _locations = new Collection<Location>();
            }
    
            public string User { get; private set; }
            public string Name { get; private set; }
            public string Photo { get; private set; }
            public DateTime When { get; set; }
            public bool Found { get; set; }
    
            public string Id
            {
                get { return BuildId(User, Name, When); } 
            }
            public IEnumerable<Location> GetLocations()
            {
                return _locations;
            }
            public Location AddLocation(Location location)
            {
                _locations.Add(location);
                return location;
            }
            public static string BuildId(
                string user, string name, DateTime now)
            {
                return string.Format(
                    "{0}#{1}#{2}", user, name, now);
            }
        }
  2. We then define our service contract, shown as follows:
        public interface IPetsFinder
        {
            Pet Lost(
                string user, string name, 
                PointF location, string photo);
            Pet Seen(string id, PointF location);
            void Found(string id);
            IEnumerable<Pet> GetPets();
        }

    These methods are there to notify about a new lost pet, add a new sighting, record a finding, and enumerate all the pets recorded in the application.

  3. Now, let's implement the contract that we just described:
        public class PetsFinder : IPetsFinder
        {
            private readonly IDictionary<string, Pet> _pets =
                new Dictionary<string, Pet> ();
    
            public Pet Lost(
                string user, string name, 
                PointF location, string photo)
            {
                var now = DateTime.UtcNow;
                var id = Pet.BuildId(user, name, now);
    
                if (_pets.ContainsKey(id)) return null;
    
                var pet = new Pet(user, name, photo, now);
                _pets.Add(id, pet);
                _pets[id].AddLocation(new Location(now,
                    location));
    
                return pet;
            }
    
            public Pet Seen(string id, PointF location)
            {
                var pet = _pets[id];
                pet.AddLocation(
                    new Location(DateTime.UtcNow, location));
    
                return pet;
            }
    
            public void Found(string id)
            {
                _pets[id].Found = true;
            }
    
            public IEnumerable<Pet> GetPets()
            {
                return
                    from id in _pets.Keys
                    let pet = _pets[id]
                    where !pet.Found
                    select pet;
            }
        }

    The service is simple, and it uses a dictionary to store information about the pets in the memory. A proper implementation would, of course, use a more resilient system. The rest of the code should be quite self explanatory.

    Before getting to see the implementation of PetsFinderHub, let's quickly implement a custom and the naïve authentication and authorization system; for this, we'll override the IUserIdProvider service.

  4. Let's add a class called UserIdProvider that contains the following code:
        public class UserIdProvider : IUserIdProvider
        {
            public string GetUserId(IRequest request)
            {
                return request.QueryString["user"];
            }
        }

    We already saw this approach earlier; we use a custom query string parameter placed on the SignalR endpoint by the client to transport that information about the current user at every request. It's a simplified approach, of course, but it's enough for us.

Now that the application logic is ready, let's use it from the application hub using the following steps:

  1. Let's add PetsFinderHub to the project:
    using System.Drawing;
    using System.Threading.Tasks;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace Recipe50
    {
        [HubName("pets")]
        public class PetsFinderHub : Hub
        {
            private readonly IUserIdProvider _userIdProvider;
            private readonly IPetsFinder _petsFinder;
    
            public PetsFinderHub(
                IUserIdProvider userIdProvider,
                IPetsFinder petsFinder)
            {
                _userIdProvider = userIdProvider;
                _petsFinder = petsFinder;
            }
    
         ...
        }
    }

    The constructor requires a reference of both IPetsFinder and IUserIdProvider contracts, which are then stored in the instance fields. We put some dots to indicate where the remaining methods will be added.

  2. We then define how we connect to the hub, shown as follows:
            public override Task OnConnected()
            {
                foreach (var pet in _petsFinder.GetPets())
                {
                    Clients.All.pet(new
                    {
                        pet.Id, pet.User, pet.Name, pet.Photo,
                        Locations = pet.GetLocations()
                    });
    
                }
                return base.OnConnected();
            }

    The OnConnected override retrieves a list of the pets who the application has already been notified about, each one with its sightings, and sends it to the caller by triggering the client side pet callback.

  3. When a pet gets lost, the user has to notify PetsFinderHub about it:
            public void Lost(
                string name, float latitude,
                float longitude, string photo)
            {
                var user =
                    _userIdProvider.GetUserId(Context.Request);
    
                var location = new PointF(latitude, longitude);
                var pet = _petsFinder.Lost(
                    user, name, location, photo);
    
                if (pet == null) return;
    
                Clients.All.pet(new
                {
                    pet.Id, pet.User, pet.Name, pet.Photo,
                    Locations = pet.GetLocations()
                });
            }

    We first ask the _userIdProvider service to resolve the name of the current user and then we supply the information about the pet who just got lost to the _petsFinder service, which will properly store it. If everything goes fine, we notify all the connected clients about this new lost animal using the pet callback.

  4. If someone has a chance to see one of the lost pets somewhere, he/she can notify the application about its new position using the following code:
            public void Seen(
                string id, float latitude, float longitude)
            {
                var location = new PointF(latitude, longitude);
                var pet = _petsFinder.Seen(id, location);
    
                Clients.All.pet(new
                {
                    pet.Id, pet.User, pet.Name, pet.Photo,
                    Locations = pet.GetLocations()
                });
            }

    After having contacted the _petsService service with the information related to the new position of a specific pet, we notify all the connected clients about it, triggering the client-side pet callback.

  5. Eventually, and hopefully, a lost pet will be found, and the Found() method, shown in the following code snippet, will allow us to notify the application about this event:
            public void Found(string id)
            {
                _petsFinder.Found(id);
                Clients.All.found(id);
            }

    Similar to the previous methods, the connected clients will be notified about the finding by the found callback.

  6. The only bits that we are still missing are the ones that are needed to bootstrap the application in the Startup class. We use the dependency injection strategies that we learned earlier in Chapter 7, Analyzing Advanced Scenarios, to put all the components together, as shown in the following code snippet:
            public void Configuration(IAppBuilder app)
            {
                GlobalHost.DependencyResolver.Register(
                    typeof(IUserIdProvider), 
                    () => new UserIdProvider());
    
                var petsFinder = new PetsFinder();
                GlobalHost.DependencyResolver.Register(
                    typeof(IPetsFinder), 
                    () => petsFinder);
                GlobalHost.DependencyResolver.Register(
                    typeof(PetsFinderHub), 
                    () => new PetsFinderHub(
                        new UserIdProvider(), petsFinder));
    
                app.MapSignalR();
            }

Now that we are done with the server-side portion of the application, let's move back to the client side to add the pets.js file to the Scripts folder and fill its content. We'll use the following steps to do this:

  1. We first add some useful variables:
    $(function () {
    
        var hub = $.connection.hub,
            pets = $.connection.pets,
            map = new google.maps.Map($("#map")[0], {
                center: new google.maps.LatLng(
                    40.760976, -73.969041),
                zoom: 14
            }),
            params = {},
            locations = {};
    
         ...
    })

    Among other things, here, we center the map on a specific location and set its initial zoom level.

  2. We add a few click event handlers to the buttons that are displayed over the map:
        $('#login').click(function() {
            params.user = $('#user').val();
            hub.qs = { user: params.user };
            hub.start() 
                .done(function () {
                    $('#nickname').text(params.user);
                    $('#login-panel').toggle();
                    $('#logged-panel').toggle();
                    $('#lost-panel').toggle();
                });
        });
        $('#proceed').click(function () {
            params.name = $('#name').val();
            $('#lost-panel-photo').toggle();
        });
        $('#lost').click(function () {
            params.adding = true;
        });
        $('#found').click(function () {
            pets.server.found(params.id);
        });

    The only interesting line here is from the login button, where we use the qs member of the hub variable to set the user member just before we start the connection. This way, we guarantee that its value will be sent to the server at every request, allowing the UserIdProvider method, which we defined earlier, to work as expected.

  3. When we are in the process of notifying about a new lost pet, one of the steps consists of providing its picture by dropping an image file on a div element called photo. The following is how this can be set up:
        $('#photo')
            .on('dragover', function () { return false; })
            .on('dragend', function () { return false; })
            .on('drop', function (s) {
                var e = s.originalEvent;
                e.preventDefault();
                drop(e.dataTransfer.files);
                $('#lost-panel-location').toggle();
            });

    Later, we'll see the drop() function in detail; here, we just underline the fact that we can easily send images back and forth with SignalR using their inline Base64-encoded representation, which is what we do here.

    Note

    At the moment of this writing, there are some limitations to this technique. In particular, strings passed to a hub cannot exceed a limit in size whose value might depend on the transport strategy. Inline images can easily exceed this limitation, and therefore, before taking architectural decisions on this feature, you should carefully verify its limitations.

  4. We then need to handle the click events on the map, which are used to mark where a pet got lost:
        google.maps.event.addListener(map, 'click', 
        function (me) {
            if (!params.adding) return;
    
            pets.server.lost(params.name,
                me.latLng.lat(),me.latLng.lng(), 
                params.image);
            params.adding = false;
    
            $('#photo').empty();
            $('#lost-panel-photo').toggle();
            $('#lost-panel-location').toggle();
        });

    All the details that have been collected about the lost pet so far are sent to the PetsFinderHub, including a string that contains the Base64-encoded image.

  5. We are ready to define SignalR's callbacks, starting from the one used to notify a new sighting:
        pets.client.pet = function (lost) {
            if (locations[lost.Id]) {
                $.each(locations[lost.Id].markers, 
                    function(mi, m) { m.setMap(null); });
            }
            locations[lost.Id] = {
                id: lost.Id, markers: []
            };
            $.each(lost.Locations, function (li, location) {
                var image = new Image();
                image.src = lost.Photo;
    
                var marker = new google.maps.Marker({
                    icon: framed(image, li ? 'green' : 'red'),
                    map: map,
                    title: lost.name,
                    draggable: true,
                    position: new google.maps.LatLng(
                        location.Where.X, location.Where.Y)
                });
                google.maps.event.addListener(marker, 
                    'click', function () {
                        var m = lost.Name +
                            ' was here, time: ' +
                            location.When;
                        $('#messages')
                            .append($('<li/>').text(m));
                        $('#found').toggle(
                            lost.User == params.user);
                        params.id = lost.Id;
                 });
                google.maps.event.addListener(marker, 
                    'dragend', function(me) {
                        pets.server.seen(lost.Id,
                           me.latLng.lat(), 
                           me.latLng.lng());
                    });
                locations[lost.Id].markers.push(marker);
            });
        };

    For each location provided along the detail of the pet, a new custom marker is created on the map. The custom marker will contain the picture of the pet, which was built using the framed() function that we'll see later, and it will allow users to interact with it in a couple of ways:

    • Click: This interaction will register the current user to receive notifications about new events regarding the pet whose picture was clicked on. Such notifications will be listed as text messages on the right-hand side of the map.
    • Drag-and-drop: A marker can be dragged around on the map and then dropped on a new position in order to notify a new location where the corresponding pet was last seen. The drop event will trigger a call towards the Seen() server-side method.
  6. The found callback will remove all the markers of the rescued pet from the map:
        pets.client.found = function (id) {
            $.each(locations[id].markers, function (mi, m) {
                m.setMap(null);
            });
            locations[id] = {};
        };
  7. Finally, we quickly list the helper functions that we used earlier:
        function drop(files) {
            var reader = new FileReader();
            reader.onload = function (event) {
                var image = new Image();
                image.src = event.target.result;
                params.image = resized(image);
                image.src = params.image;
                $('#photo').append(image);
            };
            reader.readAsDataURL(files[0]);
        }
        function resized(img) {
            var canvas = document.createElement('canvas'),
                maxWidth = 180,
                maxHeight = 180,
                size = {
                    width: img.width,
                    height: img.height
                };
                size = size.width > size.height
                     ? size.width > maxWidth
                       ? {
                          height: Math.round(
                              size.height * 
                              maxWidth / size.width),
                          width:  maxWidth
                         } : size
                     : size.height > maxHeight
                       ? {
                          width: Math.round(
                              size.width * 
                              maxHeight / size.height),
                          height: maxHeight
                         } : size;
    
            canvas.width  = size.width;
            canvas.height = size.height;
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, size.width, size.height);
            return canvas.toDataURL("image/jpeg", 0.7);
        }
        function framed(image, color) {
            var canvas = document.createElement('canvas'),
                size = {
                    width: image.width / 2 + 20,
                    height: image.height / 2 + 20
                };
    
            canvas.width = size.width;
            canvas.height = size.height;
            var ctx = canvas.getContext("2d");
    
            ctx.beginPath();
            ctx.fillStyle = color;
            ctx.rect(0, 0, size.width, size.height);
            ctx.fill();
            ctx.drawImage(
                image, 10, 10, 
                size.width - 20, size.height - 20);
    
            ctx.fillStyle = "yellow";
            ctx.beginPath();
            ctx.moveTo(size.width / 2, size.height);
            ctx.lineTo(size.width / 2 + 7, size.height - 20);
            ctx.lineTo(size.width / 2 - 7, size.height - 20);
            ctx.closePath();
            ctx.fill();
    
            return canvas.toDataURL("image/jpeg", 0.7);
        }

    The drop() function is interesting because it uses the new HTML5 FileReader API to read the content of the image file dropped onto the div element named photo. The resized() and framed() functions leverage the Base64-encoded image format and the canvas capabilities to process the pictures in order to both send them to PetsFinderHub on the server and use them to create custom map markers on the client.

We are done and are now ready to test the application by building the project and opening the index.html page in multiple windows. From each of these windows, we can log in to the system and use the simple submission wizard to notify about a lost pet. We can drag-and-drop markers around to let the application know about the new positions, and the owner of the pet can eventually declare the pet as found. This way, its markers will be removed. All these actions are constantly propagated to every connected map in real time. Clicking on a marker will subscribe the user to text messages that alert about a new sighting related to the corresponding pet.

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

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