Vinicius Quinafelex Alves

🌐Ler em português

[C#] Zone of a coordinate in a KML file

KMLs are structured XMLs files, containing geographic data for processing and rendering. Used by Google Maps, this format is considered an international standard by the OGC, international consortium related to geoprocessing, since 2008.

Zoning is a method of land division, where the land surface is sliced in non-overlapping polygons (or zones). This method is frequently used for urban planning and certain forms of geoprocessing.

To find the zone that contains a specific coordinate, the code must read all the polygons inside the KML, and check each polygon to find which one contains the coordinate. For that, this article uses the SharpKml.Core library, implemented in .NET Standard and MIT license, to read the KML file.

The library does not implement a method to check if a coordinate is within a polygon, so this article make use of an external algorithm. This algorithm projects a line based on one of the coordinate axis, and declares a flag that alternates between "inside" and "outside" everytime the line crosses one of the polygon vertices.

SharpKml.Core installation

dotnet add package SharpKml.Core

Main function

using SharpKml.Dom;
using SharpKml.Engine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
            
public static string GetZone(string kmlFilePath, double latitude, double longitude)
{
    KmlFile kmlFile = LoadKmlFile(kmlFilePath);
    IEnumerable<Placemark> placemarks = EnumeratePlacemarks(kmlFile);

    foreach(Placemark placemark in placemarks)
    {
        IEnumerable<Polygon> polygons = EnumeratePolygons(placemark);

        foreach(Polygon polygon in polygons)
        {
            CoordinateCollection coordinates = GetPolygonCoordinates(polygon);

            if(WithinPolygon(coordinates, latitude, longitude))
            {
                return GetSimpleDataValue(placemark, "ZoneID");
            }
        }
    }

    return null;
}

Auxiliary functions

private static KmlFile LoadKmlFile(string path)
{
    using (var stream = new FileStream(path, FileMode.Open))
        return KmlFile.Load(stream);
}

private static IEnumerable<Placemark> EnumeratePlacemarks(KmlFile file)
{
    return file.Root.Flatten().OfType<Placemark>();
}

private static IEnumerable<Polygon> EnumeratePolygons(Placemark placemark)
{
    var shape = placemark.Geometry;

    if (shape is Polygon)
    {
        yield return (Polygon)shape;
    }
    else if (shape is MultipleGeometry)
    {
        var multipleGeometry = (MultipleGeometry)shape;

        foreach (var geometry in multipleGeometry.Geometry)
            if (geometry is Polygon)
                yield return (Polygon)geometry;
    }
}

private static CoordinateCollection GetPolygonCoordinates(Polygon polygon)
{
    return polygon.OuterBoundary.LinearRing.Coordinates;
}

private static string GetSimpleDataValue(Placemark placemark, string attributeName)
{
    foreach (var schemaData in placemark.ExtendedData.SchemaData)
        foreach (var simpleData in schemaData.SimpleData)
            if (simpleData.Name == attributeName)
                return simpleData.Text;

    return null;
}

// Source: https://stackoverflow.com/a/14998816
public static bool WithinPolygon(CoordinateCollection coordinateCollection, double latitude, double longitude)
{
    var coordinates = coordinateCollection.ToList();

    bool result = false;
    int j = coordinates.Count - 1;

    for (int i = 0; i < coordinates.Count; i++)
    {
        var pointA = coordinates[i];
        var pointB = coordinates[j];

        if (pointA.Longitude < longitude && pointB.Longitude >= longitude || pointB.Longitude < longitude && pointA.Longitude >= longitude)
        {
            if (pointA.Latitude + (longitude - pointA.Longitude) / (pointB.Longitude - pointA.Longitude) * (pointB.Latitude - pointA.Latitude) < latitude)
            {
                result = !result;
            }
        }

        j = i;
    }

    return result;
}