ArcGIS Runtime SDK for .NET

Display KML content

Keyhole Markup Language (KML) is a geographic data format popularized by Google Earth. KML files can be distributed with supporting content, including images and 3D models, in a KMZ archive.

Display a KML/KMZ file

KML content is loaded using a KmlDataset. The KmlDataset constructor takes a URL, which can point to a local file or a network location. Loading the layer will cause the associated dataset to load. KML layers can also be loaded from portal items, which can specify a link to a file or the KML/KMZ file itself.

KmlDataset kmlDataSource = new KmlDataset(kmlFilePathUri);
KmlLayer kmlOperationalLayer = new KmlLayer(kmlDataSource);

Note:
See the licensing guide to learn more about the licensing requirements for KML files loaded over the network and from disk.

Many KML files are wrappers that point to resources over the network. For example, a weather map might consist of a single network link that points to the latest forecast, to be retrieved every 5 minutes. As a result, loading the layer does not necessarily guarantee that the content has loaded – linked content can fail to load without affecting the load status of the layer.

Note:
Many KML files, even those delivered over secure HTTPS connections, point to resources via insecure HTTP links. These resources may fail to load as a consequence of App Transport Security on iOS. Add appropriate ATS exceptions as needed for KML content. See Apple’s developer documentation for more information about ATS exceptions.

Exploring the KML content tree

KML layers contain content in a hierarchy. You may need to programmatically explore this hierarchy to interact with KML content. For example, to turn off a screen overlay, you would need to first find it in the tree, then change its visibility. This code will follow a recursive pattern, using a function that calls itself for each node in the tree. The KML tree should be explored starting with KmlDataset. KmlDataset exposes the KML feature tree with the RootNodes property. This provides a collection of KmlNodes. Start with a function that accepts an enumeration of KmlNodes.

private void ExploreKml(IEnumerable<KmlNode> nodes)
{
    foreach(KmlNode node in nodes)
    {
        // Do work here...
    }
}

The non-recursive part of turning off a screen overlay is toggling its visibility once it's found. Because you're working with a generic KmlNode, you first need to determine whether each iterated node is a screen overlay, KmlScreenOverlay. After the screen overlay is found, toggle its visibility.

private void ExploreKml(IEnumerable<KmlNode> nodes)
{
    foreach(KmlNode node in nodes)
    {
        if (node.GetType() == typeof(KmlScreenOverlay)) 
        {
            node.IsVisible = !node.IsVisible;
        }
    }
}

You should then look for screen overlay nodes that may exist deeper in the hierarchy, however. Recursively call exploreKml on any nodes that have children.

private void ExploreKml(IEnumerable<KmlNode> nodes)
{
    // Iterate all nodes.
    foreach (KmlNode node in nodes)
    {
        // Toggle node visibility for any screen overlays found.
        if (node.GetType() == typeof(KmlScreenOverlay))
        {
            node.IsVisible = !node.IsVisible;
        }

        // See if this node has child nodes; store them in a list.
        List<KmlNode> children = new List<KmlNode>();
        if (node is KmlContainer containerNode)
        {
            children.AddRange(containerNode.ChildNodes);
        }
        if (node is KmlNetworkLink networkLinkNode)
        {
            children.AddRange(networkLinkNode.ChildNodes);
        }

        // Recursively call this function with the child node list.
        ExploreKml(children);
    }
}

You can call the recursive function initially withKmlDataset's RootNodes, either after loading the map or in reaction to user action (like a button press).

The tree-exploration pattern is useful for other tasks involving KML. For example, ArcGIS Earth uses a recursive pattern to build a table of contents:

Screenshot of ArcGIS Earth's table of contents, with a KML file open.

Identify and popups in KML

In the ArcGIS information model, a popup is defined with a set of fields that describe an object, including how that information is formatted. Unlike ArcGIS feature services, KML files don't define a standard schema for object attributes. Instead, KML files may provide a rich HTML annotation for each object, which can be presented in place of a popup. You can access the the HTML annotation via KmlNode.BalloonContent, then display that using a webview.

Viewpoints in KML

KML has two primary ways for defining viewpoints:

  • LookAt - defines a camera relative to the position of a KML feature

  • Camera - defines the position of the camera explicitly

KmlViewpoint can represent both types of viewpoints. Custom code is required to convert from a KML viewpoint to an ArcGIS Runtime viewpoint, which can be used for navigating a scene. See Google's reference documentation for details, including diagrams. Earth browsing apps should respect LookAt viewpoints specified in KML content.

private async void NavigateToNode(KmlNode node)
{
    // Get a corrected Runtime viewpoint using the KmlViewpoint.
    Viewpoint runtimeViewpoint = ViewpointFromKmlViewpoint(node, out bool viewpointNeedsAltitudeAdjustment);
    if (viewpointNeedsAltitudeAdjustment)
    {
        runtimeViewpoint = await GetAltitudeAdjustedViewpointAsync(node, runtimeViewpoint);
    }

    // Set the viewpoint.
    if (runtimeViewpoint != null && !runtimeViewpoint.TargetGeometry.IsEmpty)
    {
        await MySceneView.SetViewpointAsync(runtimeViewpoint);
    }
}

private Viewpoint ViewpointFromKmlViewpoint(KmlNode node, out bool needsAltitudeFix)
{
    KmlViewpoint kvp = node.Viewpoint;
    // If KmlViewpoint is specified, use it.
    if (kvp != null)
    {
        // Altitude adjustment is needed for everything except Absolute altitude mode.
        needsAltitudeFix = (kvp.AltitudeMode != KmlAltitudeMode.Absolute);
        switch (kvp.Type)
        {
            case KmlViewpointType.LookAt:
                return new Viewpoint(kvp.Location,
                    new Camera(kvp.Location, kvp.Range, kvp.Heading, kvp.Pitch, kvp.Roll));
            case KmlViewpointType.Camera:
                return new Viewpoint(kvp.Location,
                    new Camera(kvp.Location, kvp.Heading, kvp.Pitch, kvp.Roll));
            default:
                throw new InvalidOperationException("Unexpected KmlViewPointType: " + kvp.Type);
        }
    }

    if (node.Extent != null && !node.Extent.IsEmpty)
    {
        // When no altitude specified, assume elevation should be taken into account.
        needsAltitudeFix = true;

        // Workaround: it's possible for "IsEmpty" to be true but for width/height to still be zero.
        if (node.Extent.Width == 0 && node.Extent.Height == 0)
        {
            // Defaults based on Google Earth.
            return new Viewpoint(node.Extent, new Camera(node.Extent.GetCenter(), 1000, 0, 45, 0));
        }
        else
        {
            Envelope tx = node.Extent;
            // Add padding on each side.
            double bufferDistance = Math.Max(node.Extent.Width, node.Extent.Height) / 20;
            Envelope bufferedExtent = new Envelope(
                tx.XMin - bufferDistance, tx.YMin - bufferDistance,
                tx.XMax + bufferDistance, tx.YMax + bufferDistance,
                tx.ZMin - bufferDistance, tx.ZMax + bufferDistance,
                SpatialReferences.Wgs84);
            return new Viewpoint(bufferedExtent);
        }
    }
    else
    {
        // Can't fly to.
        needsAltitudeFix = false;
        return null;
    }
}

// Asynchronously adjust the given viewpoint, taking into consideration elevation and KML altitude mode.
private async Task<Viewpoint> GetAltitudeAdjustedViewpointAsync(KmlNode node, Viewpoint baseViewpoint)
{
    // Get the altitude mode; assume clamp-to-ground if not specified.
    KmlAltitudeMode altMode = KmlAltitudeMode.ClampToGround;
    if (node.Viewpoint != null)
    {
        altMode = node.Viewpoint.AltitudeMode;
    }

    // If the altitude mode is Absolute, the base viewpoint doesn't need adjustment.
    if (altMode == KmlAltitudeMode.Absolute)
    {
        return baseViewpoint;
    }

    double altitude;
    Envelope lookAtExtent = baseViewpoint.TargetGeometry as Envelope;
    MapPoint lookAtPoint = baseViewpoint.TargetGeometry as MapPoint;

    if (lookAtExtent != null)
    {
        // Get the altitude for the extent.
        try
        {
            altitude = await MySceneView.Scene.BaseSurface.GetElevationAsync(lookAtExtent.GetCenter());
        }
        catch (Exception)
        {
            altitude = 0;
        }

        // Apply elevation adjustment to the geometry.
        Envelope target;
        if (altMode == KmlAltitudeMode.ClampToGround)
        {
            target = new Envelope(
                lookAtExtent.XMin, lookAtExtent.YMin,
                lookAtExtent.XMax, lookAtExtent.YMax,
                altitude, lookAtExtent.Depth + altitude,
                lookAtExtent.SpatialReference);
        }
        else
        {
            target = new Envelope(
                lookAtExtent.XMin, lookAtExtent.YMin,
                lookAtExtent.XMax, lookAtExtent.YMax,
                lookAtExtent.ZMin + altitude, lookAtExtent.ZMax + altitude,
                lookAtExtent.SpatialReference);
        }

        if (node.Viewpoint != null)
        {
            // Return adjusted geometry with adjusted camera if a viewpoint was specified on the node.
            return new Viewpoint(target, baseViewpoint.Camera.Elevate(altitude));
        }
        else
        {
            // Return adjusted geometry.
            return new Viewpoint(target);
        }
    }
    else if (lookAtPoint != null)
    {
        // Get the altitude adjustment.
        try
        {
            altitude = await MySceneView.Scene.BaseSurface.GetElevationAsync(lookAtPoint);
        }
        catch (Exception)
        {
            altitude = 0;
        }

        // Apply elevation adjustment to the geometry.
        MapPoint target;
        if (altMode == KmlAltitudeMode.ClampToGround)
        {
            target = new MapPoint(lookAtPoint.X, lookAtPoint.Y, altitude, lookAtPoint.SpatialReference);
        }
        else
        {
            target = new MapPoint(
                lookAtPoint.X, lookAtPoint.Y, lookAtPoint.Z + altitude,
                lookAtPoint.SpatialReference);
        }

        if (node.Viewpoint != null)
        {
            // Return adjusted geometry with adjusted camera if a viewpoint was specified on the node.
            return new Viewpoint(target, baseViewpoint.Camera.Elevate(altitude));
        }
        else
        {
            // Google Earth defaults: 1000m away and 45-degree tilt.
            return new Viewpoint(target, new Camera(target, 1000, 0, 45, 0));
        }
    }
    else
    {
        throw new InvalidOperationException("KmlNode has unexpected Geometry for its Extent: " +
                                            baseViewpoint.TargetGeometry);
    }
}