OpenGIS – Encoding KML Files in C#

KML is an OpenGIS encoding standard that allows for encoding points and shapes on a map. It’s similar to a shapefile, but a KML file’s points are based on latitude and longitude, and a shapefile’s points are based on a flattened offset coordinate system. For a lot of purposes this means KML is easier to read and generate.

Earth

Fortunately KML files also import into a lot of GIS applications and are supported by Google Earth.

KML is stored in XML, so to encode it we can use .Net’s XML serialization libraries.

For this example we are just going to be encoding an array of points, or placemarks. These consist of a name, description, and location. The locations consist of latitude, longitude, and altitude.

Here are the data models for placemarks and locations:

public class Placemark
{
    public Placemark(string rawData)
    {
        string[] tokens = rawData.Split(',');
        Name = tokens[1];
        Description = "Place ID: " + tokens[0];
        Point = new Location()
        {
            Longitude = Double.Parse(tokens[2],
                CultureInfo.InvariantCulture),
            Latitude = Double.Parse(tokens[3],
                CultureInfo.InvariantCulture),
        };
    }

    public Placemark()
    { }

    [XmlElement("name")]
    public string Name { get; set; }

    [XmlElement("description")]
    public string Description { get; set; }

    [XmlElement("Point")]
    public Location Point { get; set; }
}
public class Location
{
    private const string TOKEN = ",";

    public Location()
    {
        Altitude = 0;
    }

    [XmlIgnore()]
    public double Latitude { get; set; }
        
    [XmlIgnore()]
    public double Longitude { get; set; }

    [XmlIgnore()]
    public int Altitude { get; set; }

    [XmlElement("coordinates")]
    public string Coordinates 
    {
        get
        {
            var sCoordinates = new StringBuilder();
            sCoordinates.AppendWithToken(Longitude.ToString(), TOKEN); 
            sCoordinates.AppendWithToken(Latitude.ToString(), TOKEN);
            sCoordinates.Append(Altitude.ToString());
            return sCoordinates.ToString();
        }
        set { }
    }
}

So in the Placemark class you can see that I’m providing it a comma-separated string in the constructor. This is simply because my input data is going to be a csv row. This does not follow good dependency design, but it does make it easier to work within the confines of the small program I’ve constructed. If I ever needed to build this out, I would change this.

The first token in the input string is the description, then comes the name, longitude, and latitude. We are not specifying the altitude although that could be done easily. I simply don’t have that data. Instead I force the altitude to 0 in the Location constructor.

In the Location class the only property that needs to be serialized is the Coordinates string. This string is a comma separated string containing the longitude, latitude, and altitude in that specific order. There is a setter there, but it does not actually do anything. It’s just there to make the serializer happy.

I’m using a PlacemarkCollection class as the root of the XML being serialized. The code for that is simple.

[XmlRoot("kml")]
public class PlacemarkCollection
{
    public PlacemarkCollection()
    {
        Document = new List<Placemark>();
    }

    [XmlArrayItem("Placemark")]
    public List<Placemark> Document { get; set; }

    public void Add(Placemark toAdd)
    {
        Document.Add(toAdd);
    }
}

I’m using a basic class to transform a CSV file into a PlacemarkCollection.

public static class CSVTransformer
{
    public static string[] ReadCsvFile(string fileName)
    {
        string[] source = File.ReadAllLines(fileName);
        return source;
    }

    public static PlacemarkCollection CreatePlacemarkCollection(string[] input)
    {
        var collection = new PlacemarkCollection();
        foreach (string rawData in input)
        {
            collection.Add(new Placemark(rawData));
        }

        return collection;
    }
}

There is an Add right on the collection class to work better with the Liskov principal. This way the CSVTransformer does not need to care about how the PlacemarkCollection is implemented.

Then we have the main program which provides the workflow for the program.

public class Program
{
    private const int INPUT_FILE = 0;
    private const int OUTPUT_FILE = 1;
    private const string DEFAULT_INPUT_FILE = "input.csv";
    private const string DEFAULT_OUTPUT_FILE = "output.kml";

    private PlacemarkCollection ImportAndProcessData(string fileName)
    {
        string[] input = CSVTransformer.ReadCsvFile(fileName);
        return CSVTransformer.CreatePlacemarkCollection(input);
    }

    public Program()
    { }

    /// <summary>
    /// The first arg is input file name. 
    /// The second arg is output file name.
    /// </summary>
    static void Main(string[] args)
    {
        string inputFileName = args.Count() < 1 ? 
            DEFAULT_INPUT_FILE : args[INPUT_FILE];
        string outputFileName = args.Count() < 2 ? 
            DEFAULT_OUTPUT_FILE : args[OUTPUT_FILE];
            
        var kmlExporter = new Program();
        var data = kmlExporter.ImportAndProcessData(inputFileName);
        var serializer = new PlacemarkCollectionSerializer();
        serializer.Serialize(outputFileName, data);
    }
}

So the program allows the user to specify an input and output file if run from the command prompt. And if it is not specified then defaults are provided. It is not currently possible to have a specific output and a default input, but that optimization would be easy enough to add.

So the data is processed by the CsvTransformer, and then serialized to the output file. Super easy. The only thing we haven’t looked at is the serializer.

public class PlacemarkCollectionSerializer
{
    private const string KML_NAME_SPACE = "http://www.opengis.net/kml/2.2";

    public void Serialize(string fileName, PlacemarkCollection placemarks)
    {
        var serializer = new XmlSerializer(typeof(PlacemarkCollection),
            KML_NAME_SPACE);
        using (var stream = new FileStream(fileName, FileMode.Create))
        {
            using (var writer = new XmlTextWriter(stream, Encoding.Unicode))
            {
                var namespaces = new XmlSerializerNamespaces();
                namespaces.Add(string.Empty, KML_NAME_SPACE);
                serializer.Serialize(writer, placemarks, namespaces);
            }
        }
    }
}

It is critically important that the KML name space be added during serialization. This is at http://www.opengis.net/kml/2.2. Other than that we just use a FileStream, XmlTextWriter, and the .Net XmlSerializer to serialize the output.

There’s really nothing difficult or tricky about this whole process. KML is a well defined format and .Net’s libraries make it reasonably trivial to implement.

For more information on KML the reference is here: https://developers.google.com/kml/documentation/kmlreference
And Google provides documentation and a developer guide here:

If you currently do not offer any location services, often customers are happy to receive KML files and it is a fairly easy feature to implement.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s