Exporting ESRI Silverlight Graphic Layer to Google Earth

On my current project I was tasked with exporting our map layers to Google Earth. This was simple for the ESRI ArcGIS Server layers, since Server has out-of-the-box support for KML. However, as would be the case, we don’t use ArcGIS Server for all of our layers. We also use ESRI Graphic Layers for rendering user markup and data from various data sources. This presented a challenge to export these layers to Google Earth. Specifically, we wanted to maintain the same rendering and symbology that we used on our Silverlight maps in Google Earth.

Not only was it a challenge to generate the symbology as images that Google Earth could handle, but generating the KML was also difficult. It took some trial and error figuring out the correct KML and also to include the attribute data for the Graphic objects. After I had the symbols and the KML, I had to create a KMZ package to hold everything. There were a lot of moving parts and I used a few different technologies to put it all together. I was really amazed to find that I could do all of this client-side in Silverlight. I did not have to do anything on the server.

I first needed to generate the KML from the ESRI Graphic objects. I could have written the kml by hand, but I decided to use Google.KML library instead. It is much easier to use these objects instead of using an XmlWriter and writing the KML directly. I took the source code from the Google.KML library and compiled it into a Silverlight compatible dll. I included a reference to this in my project.

In the code snippet below, the call to converter.GetExportKML creates a Google.KML.geFeature object. The Google.KML object model follows the KML Version 2.1 specification. I create an XML writer, and call the ToKML method on the geFeature to write the KML to the XML writers underlying stream. This filestream is then added to a collection of ExportFile objects that are later added to the KMZ file.

private List<exportfile>; GenerateExportFiles()
    {
      KMLGraphicConverter converter = new KMLGraphicConverter();
      geFeature kmlFeature = converter.GetExportKML(mLayer.Graphics, mLayer.ID, 
    mLayer.ID, mUsedSymbols);
      XmlWriterSettings settings = new XmlWriterSettings();
      settings.Indent = true;
      MemoryStream stream = new MemoryStream();
      StreamWriter writer = new StreamWriter(stream);
      XmlWriter xmlWriter = XmlWriter.Create(writer, settings);
      //Create kml file
      xmlWriter.WriteStartDocument();
      xmlWriter.WriteStartElement("kml");
      kmlFeature.ToKML(xmlWriter);
      xmlWriter.WriteEndElement();
      xmlWriter.WriteEndDocument();
      xmlWriter.Flush();
      xmlWriter.Close();
      List<exportfile> files = new List<exportfile>();
      files.Add(new ExportFile()
      {
        Title = mLayer.ID,
        Path = "doc.kml",
        Data = stream.ToArray()
      });
      files.AddRange(GetImages()); //create images from symbols
      return files;
    }

A call to the GetImages() method, which is shown below creates ExportFile (images) objects for all the symbols used in the layer.

To generate the images, I used the SymbolDisplay object from the ESRI Silverlight API. For each graphic symbol used in the graphic layer, I set the Symbol property of the SymbolDisplay to the Graphic symbol. Then I had to put the SymbolDisplay in a Silverlight Popup and open the popup. While the popup was open, I created a WritableBitmap from the SymbolDisplay control. Then I closed the popup. This happens fast enough, that the user doesn’t even see it happening. The reason this needs to be done is that the SymbolDisplay control has to be put into the visual control tree in order for WritableBitmap to be able to generate an image from the control. Otherwise you get a blank image. Not elegant, but it works well. WritableBitmap is an easy way to create an image from just about any control on the screen. With bitmap in hand, I used the ImageTools library to create a png from the bitmap. This then gets put into a byte array which is later exported as a png file in the KMZ output.

private List<ExportFile> GetImages()
    {
      List<ExportFile> images = new List<ExportFile>();    
      foreach (Symbol symbol in mUsedSymbols)
      {
        SymbolDisplay symDisplay = new SymbolDisplay();
        symDisplay.Symbol = symbol;
        symDisplay.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
        symDisplay.VerticalAlignment = System.Windows.VerticalAlignment.Center;
//Need to put panel in popup and open popup, so panel ends up in control //visual tree.
//Otherwise when bitmap is rendered, panel will not include any child //controls.        
        Popup popUp = new System.Windows.Controls.Primitives.Popup();
        popUp.Child = symDisplay;
        popUp.IsOpen = true;
        popUp.UpdateLayout();
        WriteableBitmap bmp = new WriteableBitmap(symDisplay, null);
        ImageTools.Image img = ImageTools.ImageExtensions.ToImage(bmp);
        MemoryStream stream = new MemoryStream();
        ImageTools.ImageExtensions.WriteToStream(img, stream);
        images.Add(new ExportFile() {Title = symbol.GetHashCode().ToString(), 
                                     Path = string.Format("files/{0}.png", 
     symbol.GetHashCode().ToString()), 
                                     Data = stream.ToArray() 
                                    });
        stream.Dispose();
        popUp.IsOpen = false;
      }
      return images;
    }

Getting that far took a good deal of effort. Next I had to figure out how to get this to a kmz file. Luckily there are Silverlight libraries out there for creating zip files. Kmz is just a zip file with a different extension. I used the open-source (GPL) SharpZipLib library to generate a zip file. I then added the KML document and generated images to the KMZ.

public void ExportToKMZ()
    {
      List<ExportFile>; exportedFiles = GenerateExportFiles();
      MemoryStream stream = new MemoryStream();
      ZipOutputStream zipStream = new ZipOutputStream(stream);
      //add exported files to kmz.
      for(int i = 0; i < exportedFiles.Count; i++)
      {
        byte[] fileBytes = exportedFiles[i].Data;
        ZipEntry entry = new ZipEntry(string.Format("{1}", i, 
     exportedFiles[i].Path));
        entry.Size = fileBytes.Length;
        zipStream.SetLevel(9);
        zipStream.PutNextEntry(entry);
        zipStream.Write(fileBytes, 0, fileBytes.Length);
        zipStream.CloseEntry();
      }
      zipStream.Finish();
      zipStream.Close();
      ExportComplete(this, new ResponseEventArgs<byte[]>(stream.ToArray(), null));
    }

Finally, I used the SaveFileDialog to stream the generated kmz file to the user’s machine.

private void Button_Click(object sender, RoutedEventArgs e)
    {
      SaveFileDialog sfd = new SaveFileDialog()
      {      
        DefaultExt = "kmz",
        Filter = ".kmz|*.kmz";
      };
      if(sfd.ShowDialog() == true)
      {
        try
        {
          GraphicsLayerExport exp = new GraphicsLayerExport(myMap.Layers[1] as 
     GraphicsLayer);
          exp.ExportComplete += delegate(object exportSender,
                                         ResponseEventArgs<byte[]> args)
          {
            if(args.Error != null)
            {
              MessageBox.Show(args.Error.Message);
              return;
            }
            using(System.IO.Stream stream = sfd.OpenFile())
            {
              byte[] expBytes = args.Result;
              stream.Write(expBytes, 0, expBytes.Length);
              stream.Flush();
              stream.Close();
            }
          };
          exp.ExportToKMZ();
        }
        catch(Exception exc)
        {
          MessageBox.Show(exc.Message);
        }
      }
    }

This is a simplified example of how I went about exporting our layers to Google Earth. In this sample, I did not demonstrate exporting graphic lines or polygons, although we did do that on our project. Lines and polygons are easier to render, except that there are a couple of things to watch out for when exporting these. First kml only supports solid lines, so dashed lines have to be rendered differently. Also, polygon objects always show up underneath the other layers in Google Earth. We also had to export ArcGIS Server layers and layers that were rendered using SharpMap.

This post was written by:

Sky

Senior Associate

For more information on this post, the technologies discussed here, or Zekiah’s geospatial integration services, please e-mail us at contact@zekiah.com