CesiumJS Sample in WPF Application

Introduction

We'll walk through the process of integrating a CesiumJS sample into a WPF (Windows Presentation Foundation) application. CesiumJS is an open-source JavaScript library for creating 3D globes and maps. By embedding CesiumJS within a WPF application, you can leverage its powerful geospatial visualization capabilities alongside the rich UI features of WPF.

Prerequisites

  • Basic understanding of WPF and C# programming.
  • Visual Studio is installed on your machine.
  • Basic knowledge of HTML, JavaScript, and CesiumJS.

Integrating CesiumJS into a web application

Integrating CesiumJS into a web application presents a significant challenge due to its sophisticated 3D rendering and geographic data visualization capabilities. This project offers a comprehensive solution to streamline your development process. If you encounter any reference-related issues during implementation, we recommend installing the 'CefSharp.Wpf' NuGet package to address them efficiently.

This example showcases the functionality for users to inject KML files into the Cesium map. Additionally, it demonstrates how users can define location points on the map, create lines between two specified location points, focus the camera on specific areas of the Cesium map, and manage the loading and removal of KML files from the map.

Project setup

  • Create a new WPF project in Visual Studio.
  • Install the CefSharp.Wpf NuGet package to enable embedding Chromium (CEF) browser in your WPF application. CefSharp is a .NET wrapper for the Chromium Embedded Framework.
  • Ensure that your project targets the .NET Framework version compatible with CefSharp.
    .NET Framework

Embedding CesiumJS

  • Download the CesiumJS library from the official website or include it from a CDN.
  • Create an HTML file (`cesium.html`) and include the necessary CesiumJS scripts and stylesheets.
  • Write the JavaScript code to initialize the Cesium viewer and display the desired map or globe.

Integrating HTML into WPF

Add a `WebView` control to your WPF window. This control will host the Chromium browser.

Load the `cesium.html` file into the `WebView` control using the `LoadHtml` or `Load` method.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <!-- Include the CesiumJS JavaScript and CSS files -->
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    <style>
        /* Set the map container's dimensions */
        #cesiumContainer {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <div class="loader-overlay" id="loader" style="z-index:99999">
        <div class="loader"></div>
    </div>
    <div id="cesiumContainer" class="fullSize"></div>
</body>
</html>
.loader {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 2s linear infinite;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}
<script>
    // Function to show the loader
    function showLoader() {
        var loader = document.getElementById("loader");
        loader.style.display = "flex"; // Show the loader
    }

    // Function to hide the loader
    function hideLoader() {
        var loader = document.getElementById("loader");
        loader.style.display = "none"; // Hide the loader
    }

    function GetPinString(selectId) {
        var retString = "";
        switch (selectId) {
            case 1:
                retString = "data:image/png;base64,iVBORw0K**CC";
                break; //Blue Pin
            case 2:
                retString = "data:image/png;base64,iVBORw**K5CYII=";
                break; //Pink Pin
            case 3:
                retString = "data:image/png;base64,iVBORw0KGgo**5CYII=";
                break; //Red Pin
            default:
        }
        return retString;
    }

    function SetPolygonColor(type) {
        var polygonLineTypeColor = null;
        switch (type) {
            case 1:
                polygonLineTypeColor = Cesium.Color.RED;
                break;
            case 2:
                polygonLineTypeColor = Cesium.Color.BLUE;
                break;
            case 3:
                polygonLineTypeColor = Cesium.Color.MAGENTA;
                break;
            default:
                polygonLineTypeColor = Cesium.Color.ORANGE;
        }
        return polygonLineTypeColor;
    }
</script>
<script src="Javascript/CesiumJsMapImplementation.js"></script>

<script>
    var cesiumViewer = null;
    const kmlString = '';
    Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI**CHqSUFaF8';

    document.addEventListener("DOMContentLoaded", function () {
        showLoader();
        var viewer = new Cesium.Viewer('cesiumContainer'); // Default 3D mode

        // Call the C# function to retrieve the KML string asynchronously
        var animationContainer = document.querySelector('.cesium-viewer-animationContainer');
        if (animationContainer) {
            animationContainer.parentNode.removeChild(animationContainer);
        }

        var divToRemove = document.querySelector('.cesium-viewer-timelineContainer');
        if (divToRemove) {
            divToRemove.parentNode.removeChild(divToRemove);
        }

        var divToRemove = document.querySelector('.cesium-widget-credits');
        if (divToRemove) {
            divToRemove.parentNode.removeChild(divToRemove);
        }

        cesiumViewer = viewer;
        var iframe = document.getElementsByClassName('cesium-infoBox-iframe')[0];
        iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms');
        setTimeout(function () {
            var loader = document.getElementById("loader");
            loader.style.display = "none";
        }, 10000);
    });

    //Remove entire datascources from cesiumJs map
    function ReceiveKMLData(kmlString, kmlDocId) {
        RunKmlData(kmlString, kmlDocId);
    }

    //Hide show full screen
    function toggleFullscreenContainer() {
        var element = document.querySelector('.cesium-viewer-fullscreenContainer');
        if (element.style.display === 'none') {
            element.style.display = 'block'; // Show the element
        } else {
            element.style.display = 'none'; // Hide the element
        }
    }

    //Hide show top toolbar
    function toggleCesiumTopToolBar() {
        var element = document.querySelector('.cesium-viewer-toolbar');
        if (element.style.display === 'none') {
            element.style.display = 'block'; // Show the element
        } else {
            element.style.display = 'none'; // Hide the element
        }
    }

    //Add placemark dynamically on cesiumjs map as a point
    function AddDynamicLocationPoint(name, longitude, latitude) {
        const citizensBankPark = cesiumViewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            point: {
                pixelSize: 15,
                color: Cesium.Color.RED,
                outlineColor: Cesium.Color.WHITE,
                outlineWidth: 2,
            },
            label: {
                text: name,
                font: "bold 12pt Time New Roman",
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                scale: 1.0,
                outlineWidth: 2,
                fillColor: Cesium.Color.BLUE, // Set the label fill color
                outlineColor: Cesium.Color.WHITE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                pixelOffset: new Cesium.Cartesian2(0, -15), // Adjust the offset to your preference
            },
        });
    }

    //Add placemark on a cesiumjs map as a Pin
    function AddDynamicLocationBillBoard(name, longitude, latitude, pinType) {
        let pintTypeImage = GetPinString(pinType);
        const citizensBankPark = cesiumViewer.entities.add({
            position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
            //billboard location as a pin
            billboard: {
                image: pintTypeImage,
                width: 32,
                height: 32,
            },
            label: {
                text: name,
                font: "bold 12pt Time New Roman",
                style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                scale: 1.0,
                outlineWidth: 2,
                fillColor: Cesium.Color.BLUE, // Set the label fill color
                outlineColor: Cesium.Color.WHITE,
                horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                pixelOffset: new Cesium.Cartesian2(0, -15), // Adjust the offset to your preference
            },
        });
    }

    /*Procedure for Managing Camera Control on a Map*/
    function HandleCameraPositionInMap(longitude, latitude, height, heading, pitch, roll) {
        cesiumViewer.camera.flyTo({
            destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height), //Specify the longitude, latitude, and altitude of the camera.
            orientation: { //Adjust the camera's orientation
                heading: Cesium.Math.toRadians(heading),
                pitch: Cesium.Math.toRadians(pitch),
                roll: roll,
            },
        });
    }

    function AddLinesBetweenTwoLocation(longitude1, latitude1, longitude2, latitude2, polygonLineType) {
        var startLocation = Cesium.Cartesian3.fromDegrees(longitude1, latitude1);
        var endLocation = Cesium.Cartesian3.fromDegrees(longitude2, latitude2);
        var polygonLineTypeColor = SetPolygonColor(polygonLineType);
        cesiumViewer.entities.add({
            polyline: {
                positions: [startLocation, endLocation],
                outlineWidth: 3,
                outlineColor: Cesium.Color.BLACK,
                glowPower: 0.2,
                width: 5,
                outline: true,
                material: polygonLineTypeColor
            },
        });
    }

    /*Procedure to load KML file on a Map*/
    function RunKmlData(kmlContent, kmlDocId) {
        var options = {
            camera: cesiumViewer.scene.camera,
            canvas: cesiumViewer.scene.canvas,
            clampToGround: true
        };
        var parser = new DOMParser();
        var kmlDoc = parser.parseFromString(kmlContent, "text/xml");
        var dataSource = new Cesium.KmlDataSource();
        cesiumViewer.zoomTo(cesiumViewer.dataSources.add(dataSource.load(kmlDoc, options)))
        dataSource.id = kmlDocId;
    }

    //Remove All datasource from cesiumJs map
    function RemoveAllDatasource() {
        cesiumViewer.dataSources.removeAll(); // This clears all loaded data sources, including KML
    }

    /*Unload specific KMl file from MAP*/
    function UnloadKML(kmlDocsourceId) {
        var kmlDataSource = cesiumViewer.dataSources;
        cesiumViewer.dataSources._dataSources.forEach(function (dataSource) {
            if (dataSource.id == kmlDocsourceId) {
                cesiumViewer.dataSources.remove(dataSource);
            }
        });
    }
</script>

Communication between HTML and WPF

Implement JavaScript-to-C# communication using CefSharp's JavaScript Binding functionality. This allows JavaScript code running in the Chromium browser to call C# methods.

Xaml View

<Window x:Class="WPF.CesiumJs.Samples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF.CesiumJs.Samples"
        xmlns:wpfChromium="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
        mc:Ignorable="d" WindowState="Maximized"
        Title="MainWindow" Height="450" Width="800">

    <Window.Resources>
        <!-- Define a custom button style -->
        <Style TargetType="Button">
            <Setter Property="Background" Value="OrangeRed" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="Padding" Value="10,5" />
            <Setter Property="BorderThickness" Value="1" />
            <Setter Property="BorderBrush" Value="OrangeRed" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TabControl Grid.Row="0" >
            <TabItem Header="CesiumJs Map">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>

                    <StackPanel Orientation="Horizontal" Grid.Row="1">
                        <!-- Buttons... -->
                    </StackPanel>

                    <wpfChromium:ChromiumWebBrowser x:Name="ChromiumWebBrowser" Grid.Row="0" Margin="3"/>

                    <StackPanel Orientation="Horizontal" Margin="2" Grid.Row="2" Grid.ColumnSpan="15">
                        <!-- More buttons... -->
                    </StackPanel>
                </Grid>
            </TabItem>

            <TabItem Header="KML Document Display">
                <Grid>
                    <RichTextBox IsReadOnly="True" FontSize="20" FontWeight="Bold" FontFamily="Times New Roman" x:Name="kmlRichTextBox" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="30,0,0,0" VerticalScrollBarVisibility="Auto" AcceptsReturn="True"/>
                </Grid>
            </TabItem>
        </TabControl>
    </Grid>
</Window>
using CefSharp;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Documents;
using System.Xml;
using System.Xml.Linq;

namespace WPF.CesiumJs.Samples
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        #region Fields declaration
        private string kmlString;
        private string kmlString2;
        private string kmlString3;

        List<string> dynamicKmlStrings = new List<string>();
        ObservableCollection<KMLItems> kmlItemsObjColl = null;
        #endregion

        #region KML
        private void SetKML(string kmlFileName)
        {
            // Create a FlowDocument to hold the formatted KML with colored text.
            FlowDocument flowDocument = new FlowDocument();

            // Load the KML file as an XML document
            XmlDocument kmlDocument = new XmlDocument();

            if (!string.IsNullOrEmpty(kmlFileName))
            {
                if ("FirstLocationPoints.kml" == kmlFileName)
                {
                    kmlDocument.Load("FirstLocationPoints.kml");
                }
                if ("SecondLocationPoints.kml" == kmlFileName)
                {
                    kmlDocument.Load("SecondLocationPoints.kml");
                }
                if ("ThirdLocationPoints.kml" == kmlFileName)
                {
                    kmlDocument.Load("ThirdLocationPoints.kml");
                }

                FormatKmlXml(kmlDocument.DocumentElement, flowDocument.Blocks);
                kmlRichTextBox.Document = flowDocument;
            }
        }

        private void FormatKmlXml(XmlNode node, BlockCollection blocks)
        {
            if (node.NodeType == XmlNodeType.Element)
            {
                // Create a Paragraph for the element name with a different color.
                Paragraph elementNameParagraph = new Paragraph();

                // Create a Run for the element name and set its foreground color.
                Run elementNameRun = new Run("<" + node.Name.ToUpper().Trim() + ">")
                {
                    Foreground = System.Windows.Media.Brushes.Blue
                };

                // Add the Run to the Paragraph.
                elementNameParagraph.Inlines.Add(elementNameRun);

                // Add the Paragraph to the blocks collection.
                blocks.Add(elementNameParagraph);

                // Process child nodes recursively.
                foreach (XmlNode childNode in node.ChildNodes)
                {
                    FormatKmlXml(childNode, blocks);
                }

                // Create a Paragraph for the closing tag with a different color.
                Paragraph closingTagParagraph = new Paragraph();

                // Create a Run for the closing tag and set its foreground color.
                Run closingTagRun = new Run("</" + node.Name.ToUpper().Trim() + ">")
                {
                    Foreground = System.Windows.Media.Brushes.Blue
                };

                // Add the Run to the Paragraph.
                closingTagParagraph.Inlines.Add(closingTagRun);

                // Add the Paragraph to the blocks collection.
                blocks.Add(closingTagParagraph);
            }
            else if (node.NodeType == XmlNodeType.Text)
            {
                // Create a Paragraph for the text content with a different color.
                Paragraph textParagraph = new Paragraph();

                // Create a Run for the text content and set its foreground color.
                Run textRun = new Run(node.Value.ToLower().Trim())
                {
                    Foreground = System.Windows.Media.Brushes.Black
                };

                // Add the Run to the Paragraph.
                textParagraph.Inlines.Add(textRun);

                // Add the Paragraph to the blocks collection.
                blocks.Add(textParagraph);
            }
        }
        #endregion

        public MainWindow()
        {
            InitializeComponent();
            kmlItemsObjColl = new ObservableCollection<KMLItems>();

            string htmlFileContent = File.ReadAllText("CesiumJsMapIntegration.html");
            ChromiumWebBrowser.LoadHtml(htmlFileContent);

            kmlString = File.ReadAllText("FirstLocationPoints.kml");
            kmlString2 = File.ReadAllText("SecondLocationPoints.kml");
            kmlString3 = File.ReadAllText("ThirdLocationPoints.kml");

            kmlItemsObjColl.Clear();
            kmlItemsObjColl.Add(new KMLItems
            {
                Key = "#KMLDOC1",
                KmlDoc = kmlString
            });
            kmlItemsObjColl.Add(new KMLItems
            {
                Key = "#KMLDOC2",
                KmlDoc = kmlString2
            });
            kmlItemsObjColl.Add(new KMLItems
            {
                Key = "#KMLDOC3",
                KmlDoc = kmlString3
            });
        }

        // Other methods and event handlers...
    }
}

Testing and Debugging

  • Run your WPF application to ensure that the CesiumJS sample is properly displayed within the application window.
  • Use browser developer tools (available in CefSharp) to debug JavaScript code and inspect network requests.

Enhancements

  • Customize the appearance and behavior of the embedded CesiumJS map or globe to suit your application's requirements.
  • Explore additional features provided by CesiumJS, such as terrain rendering, imagery layers, and entity visualization.KML Document Display

Execute your WPF exe

  • Once satisfied with the integration and functionality, prepare your WPF application for deployment by building it in Release mode.
  • Ensure that all necessary files, including the CesiumJS library and any custom HTML, JavaScript, or CSS files, are included in the deployment package.

View of the running application

WPF

Source Code: https://github.com/OmatrixTech/CesiumJsTestingSample