SharpGIS

#GIS from a .NET developer's perspective

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.