Massimiliano's Blog - My .Net World

Tutto quello che avreste voluto sapere ma non avete mai chiesto......

marzo 2010 - Post

API di Bing: Selezione multipla dei pushpins

Salve a tutti, per un nuovo progetto interessantissimo a cui ho preso parte da poco, mi è stato chiesto di rappresentare delle informazioni su una cartina geografica, ovvero geo-localizzare degli indirizzi e creare un qualcosa che permetta di visualizzare delle informazioni necessarie agli operatori per poi interagire con esse.

Come molti di voi ben sanno il mercato sotto questo punto di vista offre molto, vedi google  o mapPoint etc etc, ma nel nostro caso abbiamo scelto le API di Bing. In dettaglio potete trovare informazioni a questo link. In pratica c’è a disposizione un SDK per interrogare e rappresentare coordinate polari terrestri su di una mappa.  La nostra scelta inoltre è ricaduta sull’SDK per Silverlight, esiste anche uno per asp.net ma con troppo codice javascript a mio avviso. Siccome la geo-localizzazione di un indirizzo avviene tramite chiamate  a webservices è necessario registrarsi per ottenere una licenza da sviluppatore in modo tale da poter interrogare con alcune limitazioni sull’accuratezza dell’indirizzo da ricercare (loro non lo dicono me ne sono accorto con semplici test  :D ); in ambienti di produzione è invece necessario acquistare una licenza che permette chiamate al webservice di geo-localizzazione senza tale limitazione, cmq per maggiori info verificate sempre sul sito microsoft.

Gli esempi dell’SDK sono molto esaustivi per cui tralascio i dettagli di come geo-localizzare un indirizzo concentrandoci sulla gestione dei  pushpins. In primis  creiamo una applicazione Silverlight e importiamo un paio di librerie: Microsoft.Maps.MapControl e Microsoft.Maps.MapControl.Common e inseriamo nella nostra pagina xaml una griglia con all’interno un oggetto di tipo Map.

Ad esempio se vogliamo rappresentare un pushpin bastano queste semplici istruzioni:

   1: Pushpin pushPin = new Pushpin();
   2: pushPin.Tag = p;//oggetto custom che contiene le info da rappresentare
   3: pushPin.Location = new Location(0, 0);
   4: pushPin.Name = p.Address;
   5: pushPin.MouseLeave += new MouseEventHandler(pushPin_MouseLeave);
   6: pushPin.MouseEnter += new MouseEventHandler(pushPin_MouseEnter);
   7: pushPin.MouseLeftButtonDown += new MouseButtonEventHandler(pushPin_MouseLeftButtonDown);
   8: objMap.Children.Add(pushPin); //objMap è la mappa terrestre

Il risultato è il seguente:

Img1

Come potete vedere dal codice ci sono dei gestori sugli eventi del mouse che possiamo utilizzare a nostra scelta ad esempio al MouseLeftButtonDown ho simulato la selezione del pushpin.

   1: private void pushPin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2: {
   3:   if (_handleNavigation)//flag di stato
   4:   {
   5:       _handledPushPinClick = true;
   6:  
   7:       Pushpin p = ((from c in pushPinsList
   8:                     where c == (sender as Pushpin)
   9:                     select c).FirstOrDefault() as Pushpin);
  10:       if (p != null)
  11:       {
  12:           if (selectedPushPin.Contains(p))
  13:           {
  14:               p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  15:               selectedPushPin.Remove(p);//lista dei pushpin selezionati
  16:           }
  17:           else
  18:           {
  19:               p.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Blue);
  20:               selectedPushPin.Add(p);
  21:           }
  22:       }
  23:       
  24:   }
  25: }

Il risultato è il seguente ovvero colorare il pushpin di blu e aggiungere il pushpin ad una lista che sarà quella dei pushpins selezionati:

ImgSelezionato

Una volta gestita la singola selezione vogliamo invece realizzare quella multipla ovvero l’effetto che si ottiene quando ad esempio in una classica applicazione di grafica teniamo premuto il tasto sinistro del mouse trascinando e creando un’area di selezione. Per fare questo dobbiamo prima di tutto disabilitare l’evento di default MousePan della mappa, infatti il click prolungato del tasto sinistro provoca la navigazione ovvero lo spostamento in un area geografica stabilita. Quindi in tale evento si disegna un rettangolo trasparente con i bordi tratteggiati e si valuta la posizione di ogni pushpin.

   1: private void objMap_MousePan(object sender, Microsoft.Maps.MapControl.MapMouseDragEventArgs e)
   2: {
   3:     e.Handled = _handleNavigation;//Prendiamo il controllo dell'evento
   4:     if (_handleNavigation && startDrawSelection)
   5:     {
   6:         if (objMap.Children.Count > 0 && objMap.Children[objMap.Children.Count - 1] is MapPolygon)
   7:             objMap.Children.RemoveAt(objMap.Children.Count - 1);
   8:  
   9:         //Disegno il rettangolo che rappresenta l'area di selezione
  10:         objMap.TryViewportPointToLocation(e.ViewportPoint, out endRect);
  11:         objMap.Children.Add(MapHelper.GetPolygonFromPoint(startRect, endRect));
  12:     }
  13: }

Oltre al MousePan dobbiamo gestire anche MouseLeftButtonDown e MouseLeftButtonUp

   1: private void objMap_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   2: {
   3:    e.Handled = _handleNavigation; //si prende il controllo dell'evento
   4:  
   5:    if (_handleNavigation)
   6:    {
   7:        startDrawSelection = false;
   8:  
   9:        if (objMap.Children[objMap.Children.Count - 1] is MapPolygon && objMap.Children.Count > 0)
  10:        {
  11:            foreach (Pushpin p in pushPinsList)
  12:            {
  13:                p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  14:            }
  15:  
  16:            selectedPushPin.Clear();
  17:  
  18:            foreach (Pushpin p in pushPinsList)
  19:            {
  20:                LocationRect pushPinRect = new LocationRect(
  21:                                                    new List<Location>(){
  22:                                                p.Location
  23:                                            });
  24:  
  25:                //Creo il rettangolo che sarà l'area di selezione e ne faccio l'intersezione con il pushpin
  26:                LocationRect selRect = new LocationRect((objMap.Children[objMap.Children.Count - 1] as MapPolygon).Locations);
  27:  
  28:                if (selRect.Intersects(pushPinRect))
  29:                {
  30:                    //Coloro di blu il pushpin in quanto è presente nell'area di selezione
  31:                    p.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Blue);
  32:                    selectedPushPin.Add(p);
  33:                }
  34:            }
  35:  
  36:  
  37:            //Nascondo l'area di selezione
  38:            objMap.Children.RemoveAt(objMap.Children.Count - 1);
  39:        }
  40:    }
  41: }

   1: private void objMap_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2: {
   3:     e.Handled = _handleNavigation;
   4:     if (_handleNavigation && !_handledPushPinClick)
   5:     {
   6:         objMap.TryViewportPointToLocation(e.GetPosition(sender as Map), out startRect);
   7:         startDrawSelection = true;
   8:  
   9:         foreach (Pushpin p in selectedPushPin)
  10:         {
  11:             p.Background = new System.Windows.Media.SolidColorBrush(defaultPushPinColor);
  12:         }
  13:         selectedPushPin.Clear();
  14:     }
  15:  
  16:     _handledPushPinClick = false;
  17: }

Infine vi posto anche la classe MapHelper che contiene una serie di utility di visualizzazione e disegno sulla mappa.

   1: public class MapHelper
   2: {
   3:  
   4:     /// <summary>
   5:     /// Calculates best map view for an array of points. Similar to VEMap.SetMapView method
   6:     /// </summary>
   7:     /// <param name="points">Array of Location objects</param>
   8:     /// <returns></returns>
   9:     public static BestView CalculateMapView(IList<Location> points, Map objMap)
  10:     {    //Calculate bounding rectangle
  11:         double maxLat = -90, minLat = 90, maxLon = -180, minLon = 180;
  12:         //default zoom scales in km/pixel from http://msdn2.microsoft.com/en-us/library/aa940990.aspx
  13:         double[] defaultScales = { 
  14:                                      78.27152, 
  15:                                      39.13576, 
  16:                                      19.56788, 
  17:                                      9.78394, 
  18:                                      4.89197, 
  19:                                      2.44598, 
  20:                                      1.22299, 
  21:                                      0.61150, 
  22:                                      0.30575, 
  23:                                      0.15287, 
  24:                                      .07644, 
  25:                                      0.03822, 
  26:                                      0.01911, 
  27:                                      0.00955, 
  28:                                      0.00478, 
  29:                                      0.00239, 
  30:                                      0.00119, 
  31:                                      0.0006, 
  32:                                      0.0003 };
  33:  
  34:         //Calculate bounding box for array of locations
  35:         for (int i = 0; i < points.Count; i++)
  36:         {
  37:             if (pointsIdea.Latitude > maxLat)
  38:                 maxLat = pointsIdea.Latitude;
  39:  
  40:             if (pointsIdea.Latitude < minLat)
  41:                 minLat = pointsIdea.Latitude;
  42:  
  43:             if (pointsIdea.Longitude > maxLon)
  44:                 maxLon = pointsIdea.Longitude;
  45:  
  46:             if (pointsIdea.Longitude < minLon)
  47:                 minLon = pointsIdea.Longitude;
  48:         }
  49:  
  50:         //calculate center coordinate of bounding box
  51:         double centerLat = (maxLat + minLat) / 2;
  52:         double centerLong = (maxLon + minLon) / 2;
  53:  
  54:         //create a Location object for the center point            
  55:         Microsoft.Maps.MapControl.Location centerPoint = new Microsoft.Maps.MapControl.Location();
  56:         centerPoint.Latitude = centerLat;
  57:         centerPoint.Longitude = centerLong;
  58:  
  59:         //want to calculate the distance in km along the center latitude between the two longitudes
  60:         double meanDistanceX = HaversineDistance(centerLat, minLon, centerLat, maxLon);
  61:  
  62:         //want to calculate the distance in km along the center longitude between the two latitudes
  63:         double meanDistanceY = HaversineDistance(maxLat, centerLong, minLat, centerLong) * 2;
  64:  
  65:         //calculates the x and y scales
  66:         var meanScaleValueX = meanDistanceX / objMap.Width;
  67:         var meanScaleValueY = meanDistanceY / objMap.Height;
  68:  
  69:         double meanScale;    //gets the largest scale value to work with
  70:         if (meanScaleValueX > meanScaleValueY)
  71:             meanScale = meanScaleValueX;
  72:         else
  73:             meanScale = meanScaleValueY;
  74:  
  75:         //intialize zoom level variable
  76:         int zoom = 1;    //calculate zoom level
  77:         for (var i = 1; i < 19; i++)
  78:         {
  79:             if (meanScale >= defaultScalesIdea)
  80:             {
  81:                 zoom = i;
  82:                 break;
  83:             }
  84:         }
  85:  
  86:         //return a BestView object with the center point and zoom level to use
  87:         return new BestView(centerPoint, zoom);
  88:     }
  89:  
  90:     /// <summary>
  91:     /// Calculate the distance in kilometers between two coordinates
  92:     /// </summary>
  93:     /// <param name="lat1"></param>
  94:     /// <param name="lon1"></param>
  95:     /// <param name="lat2"></param>
  96:     /// <param name="lon2"></param>
  97:     /// <returns></returns>
  98:     private static double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
  99:     {
 100:         double earthRadius = 6371;
 101:         double factor = Math.PI / 180;
 102:         double dLat = (lat2 - lat1) * factor;
 103:         double dLon = (lon2 - lon1) * factor;
 104:         double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(lat1 * factor) * Math.Cos(lat2 * factor) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
 105:         double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return earthRadius * c;
 106:     }
 107:  
 108:  
 109:     public static MapPolygon GetPolygonFromPoint(Location startPoint, Location endPoint)
 110:     {
 111:         MapPolygon polygon = new MapPolygon();
 112:         //polygon.Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGray);
 113:         polygon.Stroke = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Black);
 114:         polygon.StrokeDashCap = PenLineCap.Square;
 115:         polygon.StrokeDashOffset = 5;
 116:         polygon.StrokeDashArray = new DoubleCollection() { 0, 2.5 };
 117:         polygon.StrokeThickness = 1;
 118:         polygon.Opacity = 1;
 119:         polygon.Locations = new LocationCollection() { 
 120:                 new Location(startPoint), 
 121:                 new Location(endPoint.Latitude, startPoint.Longitude), 
 122:                 new Location(endPoint), 
 123:                 new Location(startPoint.Latitude, endPoint.Longitude) };
 124:  
 125:         return polygon;
 126:     }
 127:  
 128: }
 129:  
 130: public struct BestView
 131: {
 132:     public Location center; public int zoom;    //Constructor
 133:  
 134:     public BestView(Location _center, int _zoom)
 135:     {
 136:         center = _center;
 137:         zoom = _zoom;
 138:     }
 139: }

Alla fine il risultato è il seguente: quando si rilascia il tasto sinistro del mouse il rettangolo tratteggiato sparisce e i 4 pushpins si coloreranno di blu risultando selezionati.

ImgSelezionato

Alla prossima puntata…..

Posted: 20 mar 2010 0:41 da Massimiliano | con no comments
Inserito sotto: , ,