SharpGIS

#GIS from a .NET developer's perspective

Silverlight 3 Released

Silverlight 3 RTW is finally out !

Find the downloads here:

Expression Blend 3 with Sketchflow

Silverlight 3 Software Development Kit (SDK)

Silverlight 3 Tools for Visual Studio 2008 SP 1

or just go here to install the plugin: http://www.microsoft.com/silverlight/resources/install.aspx

While most of the new SL3 features has already been announced with the beta release, below are some of the new stuff that wasn’t available in the beta.

Mouse Wheel support !

We finally get native mouse wheel support with Silverlight 3. This means support for wheel in full screen, out of browser, when the DOM bridge is disabled, and most importantly no more ugly JavaScript hacks to get this working. The API is exactly the same as in WPF.

image 

 

Multi Touch Support.

Scott Guthrie announced this feature a few months ago, but it wasn’t included in the beta. Below is what this new API looks like:

image

Compared to the Surface API it looks like a fairly low-level version. The touch points you get will tell you if the user started touching, let go or is moving the finger, and also has methods for getting the element that is directly under the touch point. It’s up to the developer to then convert these gestures to something meaningful. The new behaviors API might come in handy for streamlining this.

Our prototype lab has already created some cool demos using Microsoft’s Surface table and our WPF API, and hopefully we can easily extend that to our Silverlight API as well.

 

Support for Alpha in 8bit PNG.

To me this is huge! We can finally start optimizing the download size by using PNG8 images and still have an alpha channel. Unfortunately Microsoft forgot 1, 2 and 4 bit, which a lot of optimized PNGs end up being. Hopefully that’ll come in a later version (judging from the PNG spec, there really isn’t much difference between 1, 2, 4 and 8 bits).

Out of browser changes

Out of browser configuration has been changed, and can now be configured using the UI tool in Visual Studio project properties:

image

Clear-type support

All browsers now get clear-type rendering of fonts.

Adding right click to Silverlight

In an earlier post, I created a small extension library that added several mouse gestures to Silverlight. However, the right-click only worked in Internet Explorer, and the solution was a bit of a hack. So to continue down the road of hacks, I found that there is a way to prevent the Silverlight Plugin from getting the right click event. By inserting a <div> on top of the plugin when the right mousebutton down event fires, you can prevent the mozilla browsers from getting the rightclick event. The mouseup event is then fired on this overlay div, and we remove the overlay again when the button is released or the mouse moves. Ugly but it works... The approach still requires the plugin to run in windowless mode.

Of course whether you even should be preventing the user from getting to the silverlight context menu in the first place is a whole different discussion.

I've updated the demo page, source and binary:

Try a demo!

Download binary (7 kb)

Download source (25 kb)

Using surrogate binders in Silverlight

In WPF you can bind values to any property on a DependencyObject, however in Silverlight, you can only bind to FrameworkElements. Most often when I have hit this roadblock is when I want to bind the rotation or scale of an element to a value. For instance a compass direction bounded to the current heading.

In WPF this would look like this:

<Image>
<Image.RenderTransform >
<RotateTransform Angle="{Binding Path=Heading}" />
</Image.RenderTransform>
</Image>

Unfortunately, since RotateTransform isn’t a FrameworkElement, this won’t work in Silverlight.

Enter: Attached Properties.

Attached properties are really neat when you start getting to know them, and you can do some pretty cool stuff, and still stick to all the MVC glory that the above case prevents you from. Using an custom attached property that manages rotation, I could for instance write:

<Image local:SurrogateBinder.Angle="{Binding Path=Heading}">
<Image.RenderTransform >
<RotateTransform  />
</Image.RenderTransform>
</Image>

So what does this binder look like? The first step is to declare the actual attached property, as well as a get and set method. Note that the naming of the property and the two get and set methods are important for this to work.
 
public static class SurrogateBinder
{
public static readonly DependencyProperty AngleProperty =
DependencyProperty.RegisterAttached("Angle", typeof(double),
typeof(SurrogateBinder),
new PropertyMetadata(OnAngleChanged));
public static double GetAngle(DependencyObject d)
{
return (double)d.GetValue(AngleProperty);
}
public static void SetAngle(DependencyObject d, double value)
{
d.SetValue(AngleProperty, value);
}
}

The next step is to react to when this value is being set/changed. The property declaration above references an OnAngleChanged method to call when the property changes. The idea is that when the value changes, we grab the rotate transform and set the value.
private static void OnAngleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is UIElement)
    {
        UIElement b = d as UIElement;
        if (e.NewValue is double)
        {
            double c = (double)e.NewValue;
            if (!double.IsNaN(c))
            {
                if (b.RenderTransform is RotateTransform)
                    (b.RenderTransform as RotateTransform).Angle = c;
                else 
                    b.RenderTransform = new RotateTransform() { Angle = c };
            }
        }
    }
}

In this case, the property binder is only made to work with a UIElement that wants a rotation applied. But by using a little reflection magic, we can create a completely generic binder that can set any property.
Instead of using one attached property, we use two. One for the value to bind, and another for the property to bind to. This is very similar to when you are creating animations and you both set the target you are animating and the property you are animating on. Apart from the Reflection magic, the code is pretty much the same:
 
public static class SurrogateBind
{
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.RegisterAttached("Target", typeof(string), typeof(SurrogateBind), null);
 
    public static string GetTarget(DependencyObject d)
    {
        return (string)d.GetValue(TargetProperty);
    }
 
    public static void SetTarget(DependencyObject d, string value)
    {
        d.SetValue(TargetProperty, value);
    }
 
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.RegisterAttached("Value", typeof(object), typeof(SurrogateBind),
        new PropertyMetadata(OnValueChanged));
 
    public static object GetValue(DependencyObject d)
    {
        return (object)d.GetValue(ValueProperty);
    }
 
    public static void SetValue(DependencyObject d, object value)
    {
        d.SetValue(ValueProperty, value);
    }
 
    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        string path = GetTarget(d);
        if (String.IsNullOrEmpty(path)) return;
        string[] pathElements = path.Split(new char[] { '.' });
        PropertyInfo propertyInfo = null;
        object o = d;
        for (int i = 0; i < pathElements.Length; i++)
        {
            if (o == null) break;
            string s = pathElements[i];
            int begin = s.LastIndexOf('[');
            bool isIndexed = s.EndsWith("]") && begin >= 0;
            propertyInfo = o.GetType().GetProperty(isIndexed ? s.Substring(0, s.LastIndexOf('[')) : s);
            if (propertyInfo == null) break;
            if (i < pathElements.Length - 1)
            {
                object[] index = null;
                if (isIndexed)
                {
                    index = new object[] { int.Parse(s.Substring(begin + 1, s.LastIndexOf(']') - begin - 1)) };
                }
                o = propertyInfo.GetValue(o, index);
            }
        }
        if (propertyInfo != null && propertyInfo.PropertyType == e.NewValue.GetType())
        {
            propertyInfo.SetValue(o, e.NewValue, null);
        }
    }
}

This allows us to to the same thing in XAML:
<TextBox Text="Hello World"
binders:SurrogateBind.Value="{Binding Path=Heading}" 
binders:SurrogateBind.Target="RenderTransform.Angle" >
<TextBox.RenderTransform>
<RotateTransform />
</TextBox.RenderTransform>
</TextBox>

Or if we have nested controls:
<TextBox RenderTransformOrigin="0.5,0.5"
Text="Hello Universe!"
binders:SurrogateBind.Value="{Binding Path=MoreValues.Heading}" 
binders:SurrogateBind.Target="RenderTransform.Children.Item[1].Angle" >
<TextBox.RenderTransform>
<TransformGroup>
<ScaleTransform />
<RotateTransform />
</TransformGroup>
</TextBox.RenderTransform>
</TextBox>

This approach can actually be extended to invoke methods on your control. For instance when a value changes, you can trigger a storyboard to start playing etc. This is partly something we will get with Triggers in Silverlight 3.0, but if you can’t wait, this is one way to do it. Maybe I’ll cover this in a later blogpost. but for now you can download the source-code and example here:
 

Extending Silverlight’s mouse events

If you just want the extension method library or source, jump to the bottom. If not, read on…

Silverlight is pretty limited when it comes to mouse events. Out of the box, you only have five events you can subscribe to on a given element:

Notice that there is no such thing as click, double-click, drag, right-click and mouse-wheel.

Click and double click you can create from the up/down events. If the mouse doesn’t move between up and down, its a click, and if it happens two times within a timespan, it’s a double-click. Similarly, a drag is a click with mouse movement in-between down and up. Using extension methods, we can easily extent the UIElement class with two new methods for each event: Attach[eventname] and Detach[eventname], that takes care of this tracking. I earlier described this approach for defining a double click extension. These are all types of gestures we often need, so creating a reusable library with these extensions is a no-brainer.

When it comes to mouse wheel, it gets slightly trickier. We can’t do this using existing Silverlight events. Instead we can use the JavaScript bridge to detect wheel events in the browser, and bubble them into the plugin. First we listen to the JavaScript event on the entire plugin, and then inside Silverlight we use the Enter/Leave events to track whether the mouse is over the element you are listening to events on. However there are cases where this wouldn’t work:

  • The application is running in full screen.
  • The Silverlight plugin is running with HTML access explicitly disabled.
  • The .xap file is hosted on a different domain than the page hosting it.
  • You are running the application in Silverlight 3’s Out-of-browser mode.

All of them are cases where the JavaScript bridge is disabled.

Lastly right-click. This is a whole different story. When you right-click a Silverlight page, it will should you a small context menu with a link to the Silverlight settings. This can’t be overridden, but using the same approach as for Wheel, you can intercept the event using JavaScript and prevent the bubbling to the plugin. However this only works reliably in Internet Explorer, and most secondly only if you run the application in windowless mode.

UPDATE: Right-click now also work in Mozilla browsers. See here

I took all these events, and wrapped the, into a little set of extension method, so you don’t have to write the code over and over again.

To use it, download the library (link at the bottom) and add a reference to the DLL. Then at the top of the class where you want to use the event extensions, add the following to your using statements:

using SharpGIS.MouseExtensions;

When you have done this, you will instantly get new intellisense on any object that inherits from UIElement:

image

You will see seven new Attach* and Detach* methods:

Click Mouse/Down without moving the mouse
DoubleClick Two quick clicks
Drag Mouse down, move, mouse up. Event will fire for each mouse movement.
Hover If the mouse stops over an element and stays in the same place for a short time.
KeyClick Click while the user is holding down a key.
RightClick Windowless only!
Wheel Mouse wheel

You can see a live sample of it in action here.

Assembly Library that you can reference in your project: Download binary (7.14 kb)

Sourcecode and sample application is available here:Download source (24.79 kb)

REALLY small unzip utility for Silverlight

UPDATE: See this updated blogpost.

There are quite a few libraries out there that adds zip decompression/compression to Silverlight. However, common to them all is that they add significantly to the size of the resulting .xap.

It turns out that Silverlight 2.0 already has zip decompression built-in. It uses this to uncompress the .xap files which really just are zip files with a different file extension.

There are several blog posts out there that will tell you how to dynamically load a XAP file and load it. It turns out that if you use the same approach with almost any other zip file, you can actually do the same thing, even though this is not a Silverlight XAP. I don’t think this was the original intent. but its still really neat! Here’s how to accomplish that, based on a zip file stream:

public static Stream GetFileStream(string filename, Stream stream)
{
    Uri fileUri = new Uri(filename, UriKind.Relative);
    StreamResourceInfo info = new StreamResourceInfo(stream, null);
    StreamResourceInfo streamInfo = System.Windows.Application.GetResourceStream(info, fileUri);
    if (streamInfo != null)
        return streamInfo.Stream;
    return null; //Filename not found or invalid ZIP stream
}

However, the problem is that this requires you to know before-hand what the names of the files are inside the zip file, and Silverlight doesn’t give you any way of getting that information (Silverlight uses the manifest file for reading the .xap).

Luckily getting filenames from the zip is the easy part of the ZIP specification to understand. This enabled us to create a generic ZIP file extractor in very few lines of code. Below is a small class utility class I created that wraps this all nicely for you.

public class UnZipper
{
    private Stream stream;
    public UnZipper(Stream zipFileStream)
    {
        this.stream = zipFileStream;
    }
    public Stream GetFileStream(string filename)
    {
        Uri fileUri = new Uri(filename, UriKind.Relative);
        StreamResourceInfo info = new StreamResourceInfo(this.stream, null);
        StreamResourceInfo stream = System.Windows.Application.GetResourceStream(info, fileUri);
        if(stream!=null)
            return stream.Stream;
        return null;
    }
    public IEnumerable<string> GetFileNamesInZip()
    {
        BinaryReader reader = new BinaryReader(stream);
        stream.Seek(0, SeekOrigin.Begin);
        string name = null;
        while (ParseFileHeader(reader, out name))
        {
            yield return name;
        }
    }
    private static bool ParseFileHeader(BinaryReader reader, out string filename)
    {
        filename = null;
        if (reader.BaseStream.Position < reader.BaseStream.Length)
        {
            int headerSignature = reader.ReadInt32();
            if (headerSignature == 67324752) //PKZIP
            {
                reader.BaseStream.Seek(14, SeekOrigin.Current); //ignore unneeded values
                int compressedSize = reader.ReadInt32();
                int unCompressedSize = reader.ReadInt32();
                short fileNameLenght = reader.ReadInt16();
                short extraFieldLenght = reader.ReadInt16();
                filename = new string(reader.ReadChars(fileNameLenght));
                if (string.IsNullOrEmpty(filename))
                    return false;
                //Seek to the next file header
                reader.BaseStream.Seek(extraFieldLenght + compressedSize, SeekOrigin.Current);
                if (unCompressedSize == 0) //Directory or not supported. Skip it
                    return ParseFileHeader(reader, out filename);
                else
                    return true;
            }
        }
        return false;
    }
}

Basically you create a new instance of the UnZipper parsing in the stream to the zip file. The method “GetFileNamesInZip” will provide you with a list of the file names available inside the file, that you can use to reference the file using “GetFileStream”.

Below is a simple example of using this. The contents of each file will be shown in a message box:

private void LoadZipfile()
{
    WebClient c = new WebClient();
    c.OpenReadCompleted += new OpenReadCompletedEventHandler(openReadCompleted);
    c.OpenReadAsync(new Uri("http://www.mydomain.com/myZipFile.zip"));
}
 
private void openReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    UnZipper unzip = new UnZipper(e.Result);
    foreach (string filename in unzip.GetFileNamesInZip())
    {
        Stream stream = unzip.GetFileStream(filename);
        StreamReader reader = new StreamReader(stream);
        string contents = reader.ReadToEnd();
        MessageBox.Show(contents);
    }
}

Note that some ZIP files which doesn't report file size before the file content is not supported by Silverlight, and is therefore also ignored by this class. This is sometimes the case when the zip file is created through a stream where the resulting file size is written after the compressed data. If you are dealing with those kind of zip files (seems fairly rare to me), you will need to use a 3rd party zip library that supports this.
UPDATE: See this updated blogpost.

This class will hardly add much to your resulting .xap, and I think it will cover 95% of the use cases when working with zip files.

Download class file: UnZipper.zip (1.25 kb)

Parsing a GeoRSS Atom feed using XML to LINQ in Silverlight

…or how to put a map of your blogposts on your blog.

I recently started a little pet project with a “photo a day” blog. I thought it could be fun to geocode each blogpost with to the place where each photograph was taken, and then place each photo on a map, similar to the flickr map I created earlier.

The most common way of geocoding an atom feed, is by adding a <georss:point>[latitude] [longitude]</georss:point> field for each entry. However, the blogengine I’m using currently doesn’t support that, so I will also try and find the location using a magic string in the blogpost, in this case “Location: [latitude] [longitude]”. You will notice that the posts I made so far, all have this at the end of the post. This might not be the most elegant solution to geocoding your blogposts, but it should work for any type of blog.

So the first step is to create a WebRequest that will download the feed (this could be simplified by using the WebClient, but I like the full control of the WebRequest):

System.Net.WebRequest request = System.Net.WebRequest.Create(
     new Uri("http://socaldaily.sharpgis.net/syndication.axd?format=atom", UriKind.Absolute));
request.BeginGetResponse(new AsyncCallback(createRssRequest), new object[] { request, this });

Begin get response handler:

private static void createRssRequest(IAsyncResult asyncRes)
{
      object[] state = (object[])asyncRes.AsyncState;
      System.Net.HttpWebRequest httpRequest = (System.Net.HttpWebRequest)state[0];
      Page page = (Page)state[1];
 
      if (!httpRequest.HaveResponse) { return; }
 
      System.Net.HttpWebResponse httpResponse = (System.Net.HttpWebResponse)httpRequest.EndGetResponse(asyncRes);
      if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK) { return; }
      Stream stream = httpResponse.GetResponseStream();
      page.Dispatcher.BeginInvoke(() => { page.ParseAtomUsingLinq(stream); });
}

When the request comes back, it will call our ParseAtomUsingLinq method with the response stream. The LINQ expression will select basic parameters like title, date, link, contents and if available, the georss location point.

private void ParseAtomUsingLinq(System.IO.Stream stream)
{
    System.Xml.Linq.XDocument feedXML = System.Xml.Linq.XDocument.Load(stream);
    System.Xml.Linq.XNamespace xmlns = "http://www.w3.org/2005/Atom"; //Atom namespace
    System.Xml.Linq.XNamespace georssns = "http://www.georss.org/georss"; //GeoRSS Namespace
 
    //Use LINQ to select all entries
    var posts = from item in feedXML.Descendants(xmlns + "entry")
                select new 
                {
                    Title = item.Element(xmlns + "title").Value,
                    Published = DateTime.Parse(item.Element(xmlns + "updated").Value),
                    Url = item.Element(xmlns + "link").Attribute("href").Value,
                    Description = item.Element(xmlns + "summary").Value,
                    Location = fromGeoRssPoint(item.Element(georssns + "point"))  //Simple GeoRSS <georss:point>X Y</georss.point>
                };
     foreach (var post in postsOrdered)
     {
          ESRI.ArcGIS.Geometry.MapPoint point = null;
          if (post.Location != null)
          {
               point = post.Location; //Use GeoRSS location
          }
          else
          {
               point = ExtractLocation(post.Description); //Search for location in blog content
          }
          if (point == null) continue; //We didn't find a point
          string imageSrc = ExtractImageSource(post.Description, post.Url); //try and find an image to use for symbol
          //TODO: Add points to map...
     }
}

You will notice in the above code we use three utility methods for extracting data. First we have a method that converts the simple GeoRSS format “<georss:point>X Y</georss.point>” to a point. In this case I’m using the MapPoint class from the ESRI ArcGIS Silverlight API, since I wan’t to use that to draw my entries on the map.

private ESRI.ArcGIS.Geometry.MapPoint fromGeoRssPoint(System.Xml.Linq.XElement elm)
{
    if (elm == null) return null;
    string val = elm.Value;
    string[] vals = val.Split(new char[] { ' ' });
    if (vals.Length != 2) return null;
    double x = double.NaN;
    double y = double.NaN;
    if (double.TryParse(vals[1], out x) && double.TryParse(vals[0], out y))
    {
        return new ESRI.ArcGIS.Geometry.MapPoint(x, y);
    }
    return null;
}

If this doesn’t return any results (usually if the feed is not georss enabled), we will during the loop look for the location tag in the entry content, using the following helper method:

private ESRI.ArcGIS.Geometry.MapPoint ExtractLocation(string description)
{    
    int idx = description.LastIndexOf("Location: ");
    int idx2 = description.LastIndexOf("</p>");
    double x = double.NaN;
    double y = double.NaN;
    if (idx < idx2 && idx > -1)
    {
        string sub = description.Substring(idx, idx2 - idx);
        string[] vals = sub.Split(new char[] { ' ' });
        foreach (string val in vals)
        {
            if (val[0] == 'N')
            {
                double.TryParse(val.Substring(1), out y);
            }
            else if (val[0] == 'S')
            {
                if (double.TryParse(val.Substring(1), out y)) y *= -1;
            }
            else if (val[0] == 'E')
            {
                double.TryParse(val.Substring(1), out x);
            }
            else if (val[0] == 'W')
            {
                if (double.TryParse(val.Substring(1), out x)) x *= -1;
            }
        }
        if (!double.IsNaN(x) && !double.IsNaN(y))
        {
            return new ESRI.ArcGIS.Geometry.MapPoint(x, y);
        }
    }
    return null;
}

Lastly, we will search for the first <img src=”…”/> entry in the body and use that for displaying the entries on the map:

private string ExtractImageSource(string description, string feedlink)
{
    int idxSrc = description.IndexOf("<img ");
    if (idxSrc >= 0)
    {
        int idxSrc2 = description.Substring(idxSrc).IndexOf("src=\"");
        int idxSrc3 = description.Substring(idxSrc + idxSrc2 + 5).IndexOf("\"");
        if (idxSrc2 >= 0 && idxSrc3 >= 0)
        {
            string src = description.Substring(idxSrc + idxSrc2 + 5, idxSrc3);
            Uri uri = new Uri(src, UriKind.RelativeOrAbsolute);
            if (uri.IsAbsoluteUri) return uri.AbsoluteUri;
            Uri page = new Uri(feedlink, UriKind.Absolute);
            return new UriBuilder(page.Scheme, page.Host, page.Port, src).Uri.AbsoluteUri;
        }
    }
    return null;
}

In our feed entries loop, we can now simply construct a graphic, set a few attributes that we will use for binding the look for the symbol (ie title and image) and add it to our graphics layer. The symbol I use here is the same as used for the flickr application mentioned above.

ESRI.ArcGIS.Graphic g = new ESRI.ArcGIS.Graphic()
{
    Geometry = point,
    Symbol = myFeedSymbol
};
g.Attributes.Add("Title", title);
g.Attributes.Add("ImageURI", src);
g.Attributes.Add("WebURI", link);
myGraphicsLayer.Graphics.Add(g);

You can view the blog map in action here: http://socaldaily.sharpgis.net/page/Photo-map.aspx

Note that there are several ways to geocode blogposts, and this approach only deals with the simplest version.

ESRI ArcGIS Silverlight/WPF API released

FINALLY we released the beta of our new ArcGIS client API for Silverlight and WPF the end of this week.

You can download the beta here, where you also can find links to documentation and samples. Note that the download requires an ESRI global account, which you can create for free.

Art has the details on his blog.

This his has been (well still is since it's beta :-) a great fun project to work on. We tried to design it to be similar to our JavaScript and Flex APIs, but at the same time make it more ".NET" when it made sense, or taken advantage of capabilities Silverlight had. We really look forward to the feedback from you, as well as talking to anyone who's at the ESRI Developer summit next week. I'll be there Monday through Thursday, mostly hanging out at the showcase area, so if you're there, stop by say hi and get to see the API first hand. Art and Rex will be doing an intro session Wednesday at 1pm, and I'll join them Thursday 10:15am for the advanced session.

Below is a simple flickr application that I created for the api, (source is available for download at the code gallery). Just zoom to any area of interest, hit the flickr button, and images in that area pops up. The sample demonstrates the power of templating the symbols which allows you to associate animations to states of the symbols, as well as using binding. This is really no different than the states model used in other Silverlight controls. The flickr symbols here has two states: MouseOver, which zooms the image and displays a small description at the bottom. The result box to the left uses the selection state when you hover on the features to first scale the image slightly and highlight the border, and if you stay hovering on it, will expand to full size. Click the result to center on that feature.

Silverlight 3.0 Pixel shaders

rippleeffect Silverlight 3.0 beta1 was released today. One of the new cool features is the pixel shader support, allowing you to make some really cool effects. I managed to fairly quickly convert the WPF Pixel Shader library to a Silverlight library (except a couple of effects), and successfully apply them to any UIElement.

To save you the trouble, you can grab the source for the library from here: ShaderEffectLibrary.zip (44.65 kb)

To use it, simply import the assembly namespace:

xmlns:fx=”clr-namespace:ShaderEffectLibrary;ShaderEffectLibrary"

and then apply any effect to your elements by setting the Effect property:

<Grid>
    <Grid.Effect>
        <fx:RippleEffect />
    </Grid.Effect>
</Grid>

The supported effects are: BandedSwirl, Bloom, BrightExtract, ColorKeyAlpha, ColorTone, ContrastAdjust, DirectionalBlur, Embossed, Gloom, GrowablePoissonDiskEffect, InvertColor, LightStreak, Magnify, Monochrome, Pinch, Pixelate, Ripple, Sharpen, SmoothMagnify, Swirl, Tone, Toon, and ZoomBlu.

Note that you will need to have the Silverlight 3.0 bits installed to use this.

UPDATE: The WPF Pixel Shader library has now added support for Silverlight as well: http://wpffx.codeplex.com/

Also note that if you want to take advantage of the new GPU hardware acceleration to further enhance performance, this is an opt-in feature! To get it, you have to go to the hosting html page and set the following object parameter (update: This doesn't apply to pixel shaders which are software only):

      <param name=”enableGPUAcceleration” value=”true” />

Also note that pixel shaders are software only, and if you add a shader to your hardware accelerated element, it will switch back to software.

Loading pages dynamically in Silverlight

I often find myself creating a large number of small test pages in my application, and every time I want to test one of them, I will have to go into App.Xaml.cs and change which page to load by setting the RootVisual.

Therefore I created a small class that allows you to on-the-fly change the currently loaded user control. It uses the #anchor tag in the URL so you can link directly to that page and use the back/forward buttons between your test pages. Furthermore, it will add a small unobtrusive 10x10px dropdown in the upper left corner. Clicking it will show you a list of all user controls available in your assembly, so you quickly can navigate to a specific user control.

To use it, first copy the code below, and add it to your project.

 

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
 
namespace SharpGIS
{
    public class ControlNavigator
    {
        private string currentHash;
        private System.Windows.Application application;
        private Grid rootGrid = new Grid();
        private string defaultControlName;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="ControlNavigator"/> class.
        /// </summary>
        /// <param name="application">The application to navigate.</param>
        /// <param name="defaultControl">The default control to load when no/invalid anchor is specified.</param>
        public ControlNavigator(System.Windows.Application application, UIElement defaultControl)
        {
            this.application = application;
            defaultControlName = defaultControl.GetType().FullName;
            CreateControl();
            UIElement element = null;
            if (HtmlPage.IsEnabled)
            {
                System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
                timer.Interval = TimeSpan.FromMilliseconds(500);
                timer.Tick += timer_Tick;
                currentHash = GetAnchor();
                element = LoadElement(currentHash);
                timer.Start();
            }
            ShowElement(element ?? defaultControl);
        }
 
        private void CreateControl()
        {
            Grid grid = new Grid();
            grid.Children.Add(rootGrid);
            ComboBox box = new ComboBox()
            {
                Width = 10,
                Height = 10,
                Opacity = 0.5,
                ItemsSource = GetUserControls(),
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
                Cursor = System.Windows.Input.Cursors.Hand
            };
            box.SelectionChanged += new SelectionChangedEventHandler(box_SelectionChanged);
            grid.Children.Add(box);
            this.application.RootVisual = grid;
        }
 
        private void box_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            string value = (sender as ComboBox).SelectedItem as string;
            if (HtmlPage.IsEnabled)
                HtmlPage.Window.Navigate(new Uri("#" + value, UriKind.Relative));
            else
                if (rootGrid.Children.Count == 0 || rootGrid.Children[0].GetType().FullName != value)
                    ShowElement(LoadElement(value));
        }
 
        private void timer_Tick(object sender, EventArgs e)
        {
            string hash = GetAnchor();
            if (hash != currentHash && 
                (rootGrid.Children.Count==0 || rootGrid.Children[0].GetType().FullName!=hash))
            {
                UIElement element = LoadElement(hash);
                if (element != null)
                {
                    ShowElement(element);
                }
            }
            currentHash = hash;
        }
 
        private void ShowElement(UIElement element)
        {
            if (element == null) return;
            rootGrid.Children.Clear();
            rootGrid.Children.Add(element);
        }
 
        private UIElement LoadElement(string name)
        {
            if (string.IsNullOrEmpty(name)) name = defaultControlName;
            try
            {
                return Assembly.GetExecutingAssembly().CreateInstance(name) as UIElement;
            }
            catch (System.Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(
                    string.Format("Couldn't load control '{0}':\n{1}\n{2}",
                    name, ex.Message, ex.StackTrace));
                MessageBox.Show(string.Format("Failed to load control {0}. See output for stack trace", name));
            }
            return null;
        }
 
        private static string GetAnchor()
        {
            if (HtmlPage.IsEnabled)
                return HtmlPage.Window.CurrentBookmark;
            else
                return null;
        }
 
        /// <summary>
        /// Creates a list of UserControls in this assembly that has an empty constructor
        /// </summary>
        /// <returns></returns>
        private List<string> GetUserControls()
        {
            List<string> types = new List<string>();
            Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            foreach (Type type in assembly.GetTypes())
            {
                if (type.IsSubclassOf(typeof(UserControl)) && type.GetConstructor(new Type[] { }) != null)
                    types.Add(type.FullName);
            }
            return types;
        }
    }
}

Go to Application_Startup() in App.Xaml.cs and change the code to:

new SharpGIS.ControlNavigator(this, new MyPage());
where MyPage() is the page control that you want to load by default.

Download code sample here

View online demo

Why EPSG:4326 is usually the wrong “projection”

…or why spherical coordinate systems are not flat!

One of the most common ways the round world is displayed on a map is using the simplest projection we have:

x = longitude
y = latitude

The name of this projection is “Plate Carree”, and is widely used because it is so simple. However we often seem to forget that we are talking about a projection. Therefore the spatial reference for this projection is very often (mis)referenced as a spherical coordinate system like the following for EPSG:4326:

GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],
PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]

GEOGCS denotes “Geographical Coordinate System”, meaning a spherical coordinate system using angles for latitude and longitudes, not X’s and Y’s. However, the moment we project this map onto a flat screen or piece of paper, we projected the coordinate system (using the simple formula above), so how can the spatial reference of our map be the above one?

I found that in ArcMap you can manually define a projected coordinate system that renders the same map, but correctly identifies the coordinate system as being projected. Below is the Well-known-text representation of this projection string (note how PROJCS denotes projected coordinate system, and how EPSG:4326 is defined inside it):

PROJCS["World_Plate_Carree_Degrees",GEOGCS["GCS_WGS_1984",
DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],
PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]
],
PROJECTION["Plate_Carree"],PARAMETER["False_Easting",0],
PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],
UNIT["Degree",111319.49079327357264771338267056]]

As far as I know we don’t have a spatial reference ID for the above (EPSG:54001 comes close but uses a different linear unit), so we often incorrectly start using EPSG:4326 to describe a projection. This has been done for so long, that this is now common (mal)practice. Even OGC’s WMS specification allows you request flat maps in a spherical coordinate system, even though this doesn’t really make any sense.

So why is this a problem? Well until recently it hasn’t really been a problem, mostly because it was rare that the spherical coordinate were correctly handled, and most applications assumed that the world was flat. However, Microsoft SQL Server 2008 can correctly handle spherical coordinates, and suddenly this becomes a major problem.

Below is a query on all the counties that follow the red line defined by two points. The blue polygons is the result returned. The map claims that its spatial reference is EPSG:4326. Because of the curvature Earth, a straight line between the two endpoints looks like a curve on the projected map, so the results returned seems like they don’t match with the line, but they are in fact the counties on the line between the two endpoints if you think in a spherical coordinate system.

image

The real problem here is not that the line is not really a curve (I drew the line like that because I want the features along that latitude). The problem is that the line didn’t know that it was projected, because the map it was displayed on was incorrectly set to EPSG:4326. However had the application known that this was not a spherical coordinate system, but a Plate Carree projection, it would have known that when converting from the flat coordinate system to the spherical coordinate system, it should ensure that my line follows the latitude. But because the input line already claims to be in spherical units, the application can not know that it needs to do anything to it (aside the fact that the application of course knows that this came from a flat screen map, but the business logic is of course separate from the UI).

This brings me to another even worse issue: I’d like to make the claim that almost all of the data you have that claims to be in EPSG:4326 has never been EPSG:4326 ! (point data excluded).

Example: Most of the northern US border roughly follows the 49’th latitude. Let’s for sake of argument define the whole northern border as two points, from coast to coast, 49 degrees north. If I then think that my data is in EPSG:4326 (or any other geographic coordinate system), the border will NOT be following the 49th latitude, but go along a great circle that cuts into Canada.

So let us thank Microsoft for creating a database that handles spherical coordinates correctly, and for giving us major headaches when trying to handle these things correctly in a clean clear way :-). Of course there wouldn’t be any headaches if we never mixed spherical and flat coordinate systems in the first place.