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.
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.
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
Source Code: https://github.com/OmatrixTech/CesiumJsTestingSample