A travers cet article, nous allons aborder l’implémentation de la recherche de proximité le long d’une route, autrement appelé “Corridor Search”, en utilisant le contrôle Silverlight et les Web Services de l’API Bing Maps et quelques fonctionnalités spatiales de SQL Server 2008.

Bing Maps for Enterprise API v6.2 et Silverlight

Il est à noter que cet exemple peut tout à fait être adapté pour effectuer de la recherche de proximité autour d’un point unique. Seule la partie accès aux données et plus particulièrement les procédures stockées seraient à adapter.

Le code de cet exemple sont disponibles en téléchargement à l’adresse suivanteimage :

http://www.boonaert.net/element/bingmaps/Samples/corridor/BingMaps-blog-samples-corridor.zip

 

Explications et architecture du projet utilisé :

Pour réaliser cet exemple, nous allons partir sur une solution d’exposition des données à travers des services WCF qui seront interrogés une fois l’itinéraire réceptionné depuis les web services de Bing Maps (Web Service 1.0).

L’objectif est donc de proposer une possibilité de calcul d’itinéraire et de recherche le long de cet itinéraire indiqué en cliquant sur la carte.

Pour cela, voici le résumé synthétique de ce qui est fait :

Schema des événements Bing Maps Silverlight et Web Services

Les étapes sont les suivantes :

  1. Suite aux clics de l’utilisateur, le Web Service Bing Maps est appelé pour calculer l’itinéraire entre les 2 points cliqués
  2. L’itinéraire est renvoyé avec l’ensemble du tracé de la route ainsi que les instructions d’itinéraire. Cette route est ajoutée sur la carte.
  3. a : Les informations géographiques de la route sont transmises aux services WCF exposant nos données.

    b : la requête utilisant les possibilités spatiales de SQL Server 2008 est exécutée pour récupérer les points à proximité de la route.
  4. La requête réalisée, on récupère les points correspondant et ils sont ensuite ajoutés aux contrôle Bing Maps Silverlight.

L’architecture utilisée pour le projet est la suivante :

Architecture du projet Bing Maps Corridor proximity search

 

Accès aux données :

Pour accéder aux données, nous utilisons LinqToSQL à travers un projet nommé WebDataHelper qui se chargera de simplifier l’appel à la base de données.

Une des limitations de LinqToSQL (de même pour Entity Framework) concerne les types introduits par SQL Server 2008 que sont les types géographiques (geography), géométriques (geometry) ou bien même hiérarchiques (hiearchyid).

Dès lors, plusieurs solutions, celle ici retenue consiste à accéder aux données à l’aide de procédures stockées qui renvoient les informations sur les points sans jamais utiliser ces types complexes.
Il nous suffit de définir une classe de mapping dans le designer LinqToSQL et d’ensuite glisser les procédures stockées directement sur la classe souhaitée pour changer le type de valeur de retour de la procédure stockée.

Voici les propriétés définies sur la procédure stockées référencées :

Corridor Search LinqToSQL binding properties Bing Maps

Astuce :
image Il est tout à fait possible de définir une seconde table avec les propriétés correspondants à la valeur de retour des procédures stockées pour très simplement glisser cette table dans le designer de Visual Studio.
Cela évite de définir toutes les informations manuellement.

Voici le code de la procédure stockée SQL :

DECLARE @routeGeom AS geography
DECLARE @routeGeomBuffered AS geography
SET @routeGeom = geography::STGeomFromText('LINESTRING(' + @WKT + ')', 4326)
SET @routeGeomBuffered = @routeGeom.STBuffer(@DistanceBuffer)

SELECT TOP 1000 [Id]
    ,[Title]
    ,[Description]
    ,[PictureUrl]
    ,[Location].Lat AS Latitude
    ,[Location].Long AS Longitude
FROM [SpatialSampleDb].[dbo].[GeoPoint]
WHERE Location.STIntersects(@routeGeomBuffered) = 1

Voici la vue synthétique du designer LinqToSQL de Visual Studio avec le pannel de connexion aux données :

LinqToSQL designer Bing Maps

A travers le projet WebDataHelper, la classe GeoDataHelper expose plusieurs méthodes pour accéder aux données, celles que nous utilisons est nommée : GetNearPoints(). 

Cette méthode prend en paramètre la liste des points composant la géométrie à partir de laquelle on effectue la recherche. Elle retourne une liste d’objet de type GeoPoint qui sont des représentations de données de punaises (coordonnées géographiques, titre, description et icône…)

Voici le code associé à cette méthode :

public List<GeoPoint> GetNearPoints(List<GeoPoint> points, int distance)
{

    List<GeoPoint> results = new List<GeoPoint>();
    try
    {
        string wkt = this.getTextWkt(points);
        if (!string.IsNullOrEmpty(wkt))
            results = ctx.GetNearPoints(wkt, distance)
.Select(p => GeoPointTranslator.ToGeoPoint(p)).ToList(); } catch (Exception) { // TODO: catch } return results; }

On remarque simplement l’utilisation d’une méthode privée pour transformer la liste de point en coordonnées mises à la suite pour construire la géométrie dans le code SQL.

string getTextWkt(List<GeoPoint> points)
{
    string wktTemp = string.Empty;

    string formatWkt = "{0} {1},";
    foreach (GeoPoint curPoint in points)
    {
        wktTemp += string.Format(formatWkt,
           curPoint.Longitude.ToString(CultureInfo.InvariantCulture.NumberFormat),
           curPoint.Latitude.ToString(CultureInfo.InvariantCulture.NumberFormat));
    }

    return wktTemp.Substring(0, wktTemp.Length - 1);    // remove the last comma
}

La classe statique GeoPointTranslator ne s’occupe que de la transformation des entités de type DB vers celle de la couche Service.

 

Exposition des données :

A travers le projet Web (WebFrontSample) qui inclut les pages de test de notre contrôle Silverlight, on ajoute un service WCF qui expose les données en utilisant le DataHelper dédié.

public class GeoDataPointService : IGeoDataPointService
{
    #region Private fields

    GeoDataHelper helper = new GeoDataHelper();

    #endregion

    #region IGeoDataPointService Members

    public List<GeoPoint> GetNearPointsFromString(string wkt, int distance)
    {
        return helper.GetNearPoints(wkt, distance);
    }

    public List<GeoPoint> GetNearPoints(List<GeoPoint> points, int distance)
    {
        return helper.GetNearPoints(points, distance);
    }

    #endregion
}

Ce service pourra ensuite être référencé dans le code client Silverlight afin de générer les classes proxy.

Attention, la méthode GetNearPointFromString() qui utilise directement la chaîne transmise peut être sujet à de l’injection de code.

 

Calcul d’itinéraire et affichage des données :

A cette étape, il ne nous reste qu’à exposer les informations au sein de la carte. Pour cet article, j’ai choisi d’utiliser le contrôle Silverlight de Bing Maps.

Pour l’intégration du contrôle Bing Maps au sein de l’application Silverlight, je vous reporte à cet article de Chris Pendleton qui explique dans le détail la procédure et même un peu plus pour inclure la vue StreetSide.

Voici le code déclaratif qui est utilisé :

<m:Map x:Name="map" 
        CredentialsProvider="YOUR CREDENTIAL"
        Center="47,2" ZoomLevel="5">
    <m:MapLayer x:Name="LayerRoute"/>
    <m:MapLayer x:Name="LayerCustomData" />
</m:Map>

Ici, dans un premier temps, on souhaite capturer les clics de souris afin de définir les points de départ et d’arrivée pour l’itinéraire à calculer. Pour cela, on associe l’événement de clic sur le MapControl.

Ici le code d’association (situé dans le constructeur de la page Silverlight) :

// Map events
this.map.MouseClick += 
    new EventHandler<Microsoft.Maps.MapControl.MapMouseEventArgs>(map_MouseClick);

Ici le code de la méthode associée :

void map_MouseClick(object sender, 
Microsoft.Maps.MapControl.MapMouseEventArgs e) { //clickedLocation = Location locTemp; if (this.map.TryViewportPointToLocation(e.ViewportPoint, out locTemp)) { if (countLocations == 0) { clickedLocation = locTemp; countLocations = 1; } else { // Calculate the route between the two clicked locations client.CalculateRouteCompleted += new EventHandler<RouteService.CalculateRouteCompletedEventArgs>(client_CalculateRouteCompleted); RouteRequest request = new RouteRequest(); request.Culture = this.map.Culture; request.Waypoints = new ObservableCollection<Waypoint>(); // Add points request.Waypoints.Add(new Waypoint() { Location = clickedLocation }); request.Waypoints.Add(new Waypoint() { Location = locTemp }); // Don't raise exceptions. request.ExecutionOptions = new ExecutionOptions(); request.ExecutionOptions.SuppressFaults = true; // Only accept results with high confidence. request.Options = new RouteOptions(); request.Options.RoutePathType = RoutePathType.Points; this.map.CredentialsProvider.GetCredentials( (Credentials credentials) => { //Pass in credentials for web services call. //Replace with your own Credentials. request.Credentials = credentials; // Make asynchronous call to fetch the data ... pass state object. this.client.CalculateRouteAsync(request); }); // Update for the next element countLocations = 0; } } }

Une fois le second clic réalisé, on appelle de manière asynchrone le Web Service Bing Maps de calcul d’itinéraire. Il faut bien entendu au préalable ajouter la référence au Web Service de routing situé à l’adresse suivante :

http://dev.virtualearth.net/webservices/v1/RouteService/RouteService.svc

Voici le code à utiliser ensuite pour associer l’événement de réception de la réponse du Web Service Bing Maps :

void client_CalculateRouteCompleted(object sender, 
RouteService.CalculateRouteCompletedEventArgs e) { // Clear the control this.LayerRoute.Children.Clear(); this.LayerCustomData.Children.Clear(); // Add the route to the map control this.addRoute(e); . . . }

Une fois la réponse reçue et dans ce même événement de réponse du Web Service Bing Maps, il devient alors possible d’afficher cette route sur le contrôle Bing Maps Silverlight mais également d’utiliser la géométrie retournée de la route exposée dans les résultats en paramètre.

Ces informations sont récupérées dans le code d’ajout de la route sur le MapControl, la méthode privée addRoute().

Après avoir référencé le service WCF dans le projet Silverlight, l’association de la réponse du service WCF ainsi que l’appel asynchrone sont réalisés à l’aide du code suivant :

// Request to search points near the route
dataClient.GetNearPointsCompleted += 
    new EventHandler<GeoDataService.GetNearPointsCompletedEventArgs>(dataClient_GetNearPointsCompleted);
            
if (this.routeLine.Locations.Any())
    if(this.routeLine.Locations.Count < 50)
        dataClient.GetNearPointsAsync(
            this.routeLine.Locations
.Select(p => GeoPointTranslator.ToGeoPointCoord(p)).ToList(), 5000 ); else dataClient.GetNearPointsAsync( this.routePoints
.Select(p => GeoPointTranslator.ToGeoPointCoord(p)).ToList(), 5000 );

La récupération des points s’effectue également de manière asynchrone à travers la méthode associé dans le code ci-dessus :

void dataClient_GetNearPointsCompleted(object sender, 
GeoDataService.GetNearPointsCompletedEventArgs e) { if (e.Result != null) { foreach (var curPoint in e.Result) { Pushpin curPin = new Pushpin() { Location = new Location() { Latitude = curPoint.Latitude, Longitude = curPoint.Longitude }, Tag = curPoint.Title + " " + curPoint.Description }; this.LayerCustomData.Children.Add(curPin); } } }

Les informations sont ajoutées sur le contrôles à travers les calques ajoutés sur le MapControl à l’aide du code déclaratif XAML.

 

Conclusions et évolutions :

Cette exemple présente l’approche que l’on peut avoir pour apporter des fonctionnalités à l’utilisateur final tout en intégrant nos propres données.

Des cas concrets d’utilisations peuvent être déduits notamment pour afficher des stations essences ou des POIs divers le long d’un parcours de vacances…

Cet exemple n’est pas forcément parfait, en effet il présente un souci lorsque la route renvoyée est trop longue.
Le transport de la géométrie complète de la route pour établir la recherche est quelque chose de mal adapté car les informations sont trop volumineuses. Plusieurs pistes pour corriger : traiter par bloc de route simplifiée ou encore modifier l’encodage des données de géométrie.

En résumé, cet exemple présente comment utiliser une manière asynchrone pour charger des données depuis de multiples services en fonction des événements générés client-side, c’est un cas pratique réutilisable dans de nombreux scénarii.

 

Le prochain article technique arrive, il concernera pour sûr, la suite de l’utilisation de MS AJAX et de Bing Maps pour le chargement des données.