Displaying WMS Legends in Silverlight with a WCF Service

This may be re-inventing the wheel big time, but whilst working on an ArcGIS Silverlight app making heavy use of WMS Layers, I could not, for the life of me, find a way  to display the WMS legend in the ‘standard’ legend control. Now, there may be a version of API I missed, or plain dump-ness but I came up with my own solution, using the ‘raw’ GetLegendGraphic request. If not anything else, this post would at least show how to retrieve dynamic images at run time in Silverlight via WCF.

First thing we need to do is to create the WCF Service (called WMSService in this example). In the IWMSService.cs file add the LegendGraphic Operation Contract

  1. [ServiceContract]
  2.     public interface IWMSService
  3.     {
  4.         [OperationContract]
  5.         Stream GetLegendGraphic(string url);
  6.     }

And in the WMSService.svc.cs the following code:

  1. [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
  2.     public class WMSService : IWMSService
  3.     {
  4.        public Stream GetLegendGraphic(string url)
  5.         {
  6.             Stream str = null;
  7.             HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
  8.             HttpWebResponse response = request.GetResponse() as HttpWebResponse;
  9.             str = response.GetResponseStream();
  10.             return str;
  11.         }
  12.     }

Add the service reference to your Silverlight project and add the following function. This will create a StackPanel including the sub-layer name, a slider to change the layer’s opacity and the layer’s legend image . You can then add the StackPanel control onto your application as required.

Create WMS Legend Control
  1. private StackPanel createWMSLayerControl(WmsLayer wmsLyr, string subLayer)
  2.         {
  3.             StackPanel spV = new StackPanel();
  4.             StackPanel spH= new StackPanel();
  5.             spH.Orientation = Orientation.Horizontal;
  6.             TextBlock tb = new TextBlock();
  7.             tb.Padding = new Thickness(5);
  8.             tb.FontWeight = FontWeights.Bold;
  9.             tb.Text = wmsLyr.ID;
  10.             Slider sl = new Slider()
  11.             {
  12.                 Maximum = 1
  13.                ,Width=50
  14.             };
  15.             string[] subLyrs= new string[1];
  16.             subLyrs[0]=subLayer;
  17.             wmsLyr.Layers = subLyrs;
  18.             sl.Tag = wmsLyr;
  19.             sl.Value = wmsLyr.Opacity;
  20.             sl.ValueChanged+=new RoutedPropertyChangedEventHandler<double>(sl_ValueChanged);
  21.             spH.Children.Add(tb);
  22.             spH.Children.Add(sl);
  23.             spV.Children.Add(spH);
  24.             //Get Legend Graphic
  25.             WMSServiceClient service = new WMSServiceClient();
  26.             service.GetLegendGraphicCompleted += new EventHandler<GetLegendGraphicCompletedEventArgs>(service_GetLegendGraphicCompleted);
  27.             TextBlock tbl = new TextBlock();
  28.             tbl.Padding = new Thickness(5);
  29.             tbl.FontStyle = FontStyles.Italic;
  30.             tbl.Text = subLayer;
  31.             spV.Children.Add(tbl);
  32.             Image imgb = new Image();
  33.             spV.Children.Add(imgb);
  34.             string strGetLegend = wmsLyr.Url + “?REQUEST=GetLegendGraphic&VERSION=” + wmsLyr.Version + “&FORMAT=image/png&WIDTH=16&HEIGHT=16&LAYER=” + subLayer;
  35.             service.GetLegendGraphicAsync(strGetLegend, imgb);
  36.             return spV;
  37.         }

Note that the GetLegendGraphicAsync  includes a second argument (userState) as the Image control. The GetLegendGraphicCompleted is where the Image Source is set (as the result of the GetLegendGraphic request).

Set the Legend Graphic
  1. void service_GetLegendGraphicCompleted(object sender, GetLegendGraphicCompletedEventArgs e)
  2.         {
  3.             int lengthInBytes = Convert.ToInt32(e.Result.Length);
  4.             byte[] imageData = e.Result;
  5.             byte[] buffer = new byte[lengthInBytes];
  6.             System.IO.MemoryStream stream = new System.IO.MemoryStream(imageData, 0, imageData.Length, false, true);
  7.             System.Windows.Media.Imaging.BitmapImage bmp = new System.Windows.Media.Imaging.BitmapImage();
  8.             bmp.SetSource(stream);
  9.             Image imgb = (Image)e.UserState;
  10.             imgb.Source = bmp;
  11.         }

Finally, the sl_ValueChange will simply allow the user to change the layer’s opacity by dragging the slider.

Change layer’s opacity
  1. void sl_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
  2.         {
  3.             Slider sl = (Slider)sender;
  4.             WmsLayer lyr = (WmsLayer)sl.Tag;
  5.             lyr.Opacity = sl.Value;
  6.         }

As an example, assuming your WMS Layer is set as:

WMS Layer Example
  1. <esri:WmsLayer ID=”World Mineral Deposits”
  2.                          Url=”;
  3.                          SkipGetCapabilities=”True” Layers=”GSC:WORLD_AgeRockDomain,GSC:WORLD_AgeDomains”
  4.                          Version=”1.1.0″ Visible=”True”
  5.                          Opacity=”0.7″ />

The legend for the first sub-layer (GSC:WORLD_AgeRockDomain) would look something like this:


And this is basically it. As mentioned in the beginning of this post- this may be a complete overkill, so if you know how to do this the ‘ArcGIS way’ please do tell!!!


Identify Task for WMS layers in ArcGIS Silverlight

Apparently it doesn’t seem to be working as expected- even if the WMS layer is stored in an MXD document.  Instead of returning the layer attributes, it returns the WMS GetFeatureInfo request i.e. something similar to:


The workaround is to use this URL to make a direct call to the WMS service and then return the results with an asynchronous request. And since the Identify task is an asynchronous request itself, you have to get one asynchronous request to call another asynchronous request. Clear as mud – right?

This example is based on the Identify Task Silverlight sample so I will assume you have the ‘standard’ identify functionality working.

The ‘first thing you need to do is create a new WCF Service which would be the one that will make the WMS GetFeatureInfo request. Lets imaginatively call it ‘WMSService’. Its Service Contract would be:

  1. [ServiceContract]
  2.     public interface IWMSService
  3.     {
  4.         [OperationContract]
  5.         string GetFeatureInfo(string url);
  6.     }

And the WMSService.svc.cs file would look like:

  1. [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
  2.     public class WMSService : IWMSService
  3.     {
  4.         public string GetFeatureInfo(string url)
  5.         {
  6.             HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
  7.             using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
  8.             {
  9.                 StreamReader reader = new StreamReader(response.GetResponseStream());
  10.                 return (reader.ReadToEnd());
  11.             }
  12.         }
  13.     }

Next we need to get back to our Silverlight project and add a reference to the new WCF Service:

  1. using WMSServiceReference;

Then, replace the cb_SelectionChanged event – remember I am assuming you are using the original Identify sample- with the following:

  1. private void cb_SelectionChanged(object sender, SelectionChangedEventArgs e)
  2.         {
  3.             int index = IdentifyComboBox.SelectedIndex;
  4.             if (index > -1 && index < identifyItems.Count)
  5.             {
  6.                 IDictionary<string, object> di = _dataItems[index].Data;
  7.                 object url = string.Empty;
  8.                 if (di.TryGetValue(“Url”, out url) || di.TryGetValue(“URL”, out url) || di.TryGetValue(“url”, out url))
  9.                 {
  10.                     string url1 = ((string)url).Replace(“text/html”, “text/xml”);
  11.                     WMSServiceClient wmsClient = new WMSServiceClient();
  12.                     wmsClient.GetFeatureInfoCompleted += new EventHandler<GetFeatureInfoCompletedEventArgs>(wmsClient_GetFeatureInfoCompleted);
  13.                     wmsClient.GetFeatureInfoAsync(url1);
  14.                 }
  15.                 else
  16.                 {
  17.                     IdentifyDetailsDataGrid.ItemsSource = _dataItems[index].Data;
  18.                 }
  19.             }
  20.         }

Notice that before I send the FeatureInfo request on its merry way, I am changing the requested format from html to xml which would be easier handled in Silverlight, rather than an HTML snippet.

So when the request completes, it will hit the GetFeatureInfoCompleted event. The response will get returned as an XML string, and the problem now is how to display this in the Identify grid since we don’t know anything about the number or type of attributes that will get returned.

  1. void wmsClient_GetFeatureInfoCompleted(object sender, GetFeatureInfoCompletedEventArgs e)
  2.         {
  3.             string res = e.Result;
  4.             IdentifyDetailsDataGrid.ItemsSource = GenerateData(res).ToDataSource();
  5.         }

The magic here occurs via the GenerateData and ToDataSource functions. Both of them have been taken from this excellent blog post by Vladimir Bodurov:  How to Bind Silverlight DataGrid From IEnumerable of IDictionary by Transforming Each Dictionary Key Into a Property of Anonymous Typed Object. I changed the GenerateData function to recursively loop through the returned XML as follows:

  1. public IEnumerable<IDictionary> GenerateData(string xmlResponse)
  2. {
  3.     var dict = new Dictionary<string, object>();
  4.     XDocument xmldoc = XDocument.Parse(xmlResponse);
  5.     XElement root = xmldoc.Root;
  6.     foreach (XNode node in root.Nodes())
  7.     {
  8.         dict = RecurseXmlDocument((XNode)node, dict);
  9.         yield return dict;
  10.     }
  11. }
  12. private static Dictionary<string, object> RecurseXmlDocument(XNode root, Dictionary<string, object> dict)
  13. {
  14.     if (root is XElement)
  15.     {
  16.         XElement el = (XElement)root;
  17.         if (el.HasElements)
  18.             RecurseXmlDocument(el.FirstNode, dict);
  19.         else
  20.         {
  21.             string strFieldName = el.Name.ToString();
  22.             string strFieldValue = el.Value;
  23.             dict[strFieldName] = strFieldValue;
  24.             if (el.NextNode != null)
  25.             {
  26.                 RecurseXmlDocument(el.NextNode, dict);
  27.             }
  28.             return dict;
  29.         }
  30.     }
  31.     return dict;
  32. }

You can download the DataSourceCreator.cs here.

Once finished you should be able to use the Identify tool on any WMS layer and get back the results. Well, ‘any’ may be an overstatement as I only tried it with a couple!


Είναι το WMS ξεπερασμένο;

Διάβασα αυτό το post και το βρήκα εξαιρετικά ενδιαφέρον. Με λίγα λόγια, η συγκεκριμένη blogger υποστηρίζει ότι το WMS (Web Map Service) – και όσους δεν ξέρουν τι είναι αυτό δείτε εδώ– είναι πια νεκρό, και ότι θα πρέπει να αρχίσουμε να χρησιμοποιούμε τους Tile Servers. Με τους Tile Servers τα γεωγραφικά δεδομένα έχουν προ-επεξεργαστεί (αν και η επεξεργασία αυτή μπορεί να γίνει και δυναμικά) και βρίσκονται σε μορφή   έτοιμων “πλακιδιών”  (tiles) και άρα και πιο γρήγορη είναι η προβολή χαρτών άλλα και πιο εύκολος ο τυχόν προγραμματιαμός που θα χρειαστεί. Ένα παράδειγμα εφαρμογής με Tile Servers είναι το Bing Maps.

Όλο το άρθρο εδώ στα ελληνικά μέσω Google Translate (και αν αναρωτηθείτε, το “κεραμίδι εξυπηρετητής“ είναι ο Tile Server!)

Οι ορθοφωτοχάρτες της ΚΤΗΜΑΤΟΛΟΓΙΟ Α.Ε.

ΕΝΗΜΕΡΩΣΗ: Δείτε αυτό το post για την ενημερωμένη εφαρμογή της ΚΤΗΜΑΤΟΛΟΓΙΟ.

Η Κτηματολόγιο Α.Ε. από τις 25 Μαϊου του 2010, προσφέρει μέσω της ιστοσελίδας της τη δυνατότητα ηροβολής ορθοφωτογραφιών που καλύπτουν το σύνολο της χώρας. Τους ορθοφωτοχάρτες μπορείτε να τους δείτε είτε μέσω της WebGIS εφαρμογής της εταιρείας στη διεύθυνση http://gis.ktimanet.gr/wms/ktbasemap/default.aspx είτε μέσω του GIS που χρησιμοποιείτε με τη χρήση Web MapServices.

Επιτέλους, ένας φορέας του Δημοσίου δίνει τα δεδομένα που μάζεψε με χρήματα των φορολογούμενων (και της Ευρωπαϊκής Ένωσης) πίσω στους ίδιους του φορολογούμενους. Δυστυχώς, αυτή η καλή προσπάθεια χαλάει από τη προχειρότητα που είναι έντονα εμφανής στην εφαρμογή WebGIS.

Και εξηγούμαι:

Όλο το layout της εφαρμογής δείχνει ερασιτεχνικό. Κατά πάσα πιθανότητα χρησιμοποιήθηκαν τα default templates του ArcGIS(;). Το περιθώριο γύρω από τα εικονίδια είναι πολύ μεγαλύτερο από τα ίδια τα εικονίδια στην εργαλειομπάρα. Η μικρή εργαλειομπάρα πάνω στο χάρτη είναι πολύ μικρή με αποτέλεσμα ο χρήστης να πρέπει να προσέχει που θα κάνει το κλικ. Και τι ακριβώς συμβαίνει με τα εργαλεία μέτρησης αποστάσεων και γραμμών; Στον ΙΕ8 (δουλεύει στον Mozilla και στο Chrome), όταν αρχίζεις να κάνεις κλικ στο χάρτη τίποτα δε φαίνεται. Καμμία “ελαστική” γραμμή που να ακολουθεί το ποντίκι. Πρέπει να μαντέψεις στο περίπου που είσαι και που έκανες κλικ. Και οι απόστασεις και τα εμβαδά παρουσιάζονται μόνο τη τελευταία στιγμή μετά το τελευταίο κλικ.

Δεν υπάρχει καμμία δυνατότητα αναζήτησης στο χάρτη. Με όλες τις δωρεάν δυνατότητες διευθυνσιοδότησης μέσω των ΑΡΙ που προσφέρουν σήμερα εταιρείες όπως η Google (Google Maps) και η Microsoft (Bing Maps), το μόνο που θα χρειαζόταν θα ήταν κάποιος να μπει στο κόπο να βάλει δύο τρία πεδία διευθύνσεων και ένα κουμπί αναζήτησης, το οποίο θα έκανε μια κλήση στην αντίστοιχη υπηρεσία/Web Service ωστε ο χάρτης να εστίαζε στη διεύθυνση.

Βεβαια και για να μη θεωρηθώ τελείως αντιδραστικος(!), η ιδέα του να μπορείς να φέρνεις χάρτες από τη Κτηματολόγιο κατευθείαν στον υπολογιστή σου μέσω Web Map Services (WMS) είναι πάρα πολύ καλή, και νομίζω η πρώτη φορά που γινεται κάτι τέτοιο στην Ελλάδα.

Εν κατακλείδι, πολύ καλή προσπάθεια, αλλά αυτό ακριβώς- προσπάθεια. Ελπίζω αυτό να αλλάξει στο μέλλον τόσο στη Κτηματολόγιο όσο και στις υπόλοιπες εταιρείες του Δημοσίου που (έχουν τη δυνατότητα να) προσφέρουν γεωχωρικά δεδομένα.