Drawing tools


As part of providing the measurement widget we've also added access to some drawing utilities for making your own tools.

Today this includes the polygon and polyline drawing tools.


It is recommended to give the user some control over the ground occlusion state while tools are active.
Within our web navigator we allow pressing "g" on the keyboard to toggle it.

Here is how Cesium code lets you toggle it:

Typescript
Javascript
import * as Cesium from "cesium";

const container = document.getElementById("VIEWER");
const viewer = new Cesium.Viewer(container);

// Hide Entities that are clipping or below ground.
viewer.scene.globe.depthTestAgainstTerrain = true;

// Show Entities that are clipping or below ground.
viewer.scene.globe.depthTestAgainstTerrain = false;

Polyline

Here is how you can enable and manage the polyline drawing tool.

The positions it works with are Cesium.Cartesian3. Read to the end of this page to learn how to turn that into latitude and longitude information.

Typescript
Javascript
import * as Cesium from "cesium";
import { Draw3dPolyline, ViewerUtils } from "bruce-cesium";

const container = document.getElementById("VIEWER");
const viewer = new Cesium.Viewer(container);
ViewerUtils.InitViewer({ viewer });

const drawPolyline = new Draw3dPolyline({
    viewer: viewer,
    onFinish: (points3d) => {
        console.log("Finished drawing polyline", points3d);

        // You can also call .Start() again if you'd prefer to restart the tool.
        drawPolyline.Dispose();
    },
    onChange: (points3d) => {
        // Called whenever the shape changes.
    },
    // (Optional) positions to start the tool with.
    posses: null,
    // (Optional) default = false, if false then it will be clamped to ground.
    perPositionHeight: false
});
drawPolyline.Start();

// To kill the tool use:
// drawPolyline.Dispose();

// To request current positions use:
const pos3ds = drawPolyline.GetPositions();

// Pausing a tool stops updates but keeps the Entities alive.
// To pause use:
// drawPolyline.Pause();

Polygon

Here is how you can enable and manage the polygon drawing tool.

The positions it works with are Cesium.Cartesian3. Read to the end of this page to learn how to turn that into latitude and longitude information.

Typescript
Javascript
import * as Cesium from "cesium";
import { Draw3dPolygon, ViewerUtils } from "bruce-cesium";

const container = document.getElementById("VIEWER");
const viewer = new Cesium.Viewer(container);
ViewerUtils.InitViewer({ viewer });

const drawPolygon = new Draw3dPolygon({
    viewer: viewer,
    onFinish: (hierarchy) => {
        console.log("Finished drawing polygon", hierarchy.positions);

        // You can also call .Start() again if you'd prefer to restart the tool.
        drawPolygon.Dispose();
    },
    onChange: (hierarchy) => {
        // Called whenever the shape changes.
    },
    // (Optional) hierarchy to start the tool with.
    hierarchy: null,
    // (Optional) default = false, if false then it will be clamped to ground.
    perPositionHeight: false,
    // (Optional) Max number of points to allow user to draw. Default is infinite.
    maxPoints: null
});
drawPolygon.Start();

// To kill the tool use:
// drawPolygon.Dispose();

// To request current shape use:
const pos3ds = drawPolygon.GetHierarchy();

// Pausing a tool stops updates but keeps the Entities alive.
// To pause use:
// drawPolygon.Pause();

2D cursor into 3D position

If you need more or would like to develop your own tools. It is recommended to use our GetAccuratePosition method instead of Cesium directly.

This is because Cesium has a number of bugs where it may pick the position through the terrain and miss the mark by over 10km.

Our method uses some techniques to make this a rare occurrence.


Here is an example on how you can place 3D points on the map.

Typescript
Javascript
import * as Cesium from "cesium";
import { DrawingUtils, ViewerUtils } from "bruce-cesium";

const container = document.getElementById("VIEWER");
const viewer = new Cesium.Viewer(container);
ViewerUtils.InitViewer({ viewer });
const canvas = viewer.canvas;

// Create an event handler. You can have multiple of these at a time.
// Running .destroy() on one will remove all this specific instance is associated with.
const eventHandler = new Cesium.ScreenSpaceEventHandler(canvas);

// Lets show a preview on where the user would place a point.
// This will keep updating "last3dPos" as the user moves their cursor around the 3D scene.
let last3dPos: Cesium.Cartesian3 = null;
eventHandler.setInputAction((e) => {
    const pos2d = e.endPosition;
    // Check to make sure 2d position is valid.
    if (pos2d?.x) {
        const pos3d = DrawingUtils.GetAccuratePosition(viewer, pos2d);
        if (!pos3d?.x) {
            return;
        }
        last3dPos = pos3d;
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

// Now let's make a Cesium Entity and make it follow this 3D position.
// "last3dPos" can be null. This will make the Entity disappear.
// But if it's an invalid 3d position it will crash cesium, this is why we validate it before updating it.
viewer.entities.add({
    // Passing a callback means it will go read the value from the variable every render.
    position: new Cesium.CallbackProperty(() => last3dPos, false) as any,
    point: {
        pixelSize: 10,
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2,
        heightReference: Cesium.HeightReference.NONE,
        show: true
    }
});

// Now let's create a new point when the user clicks.
eventHandler.setInputAction((e) => {
    const pos2d = e.position;
    if (pos2d?.x) {
        const pos3d = DrawingUtils.GetAccuratePosition(viewer, pos2d);
        if (!pos3d?.x) {
            return;
        }

        viewer.entities.add({
            position: pos3d,
            point: {
                pixelSize: 10,
                color: Cesium.Color.BLUE,
                outlineColor: Cesium.Color.WHITE,
                outlineWidth: 2,
                heightReference: Cesium.HeightReference.NONE,
                show: true
            }
        });
    }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Drawing utilities

A common need is conversion between cartesian3 and cartographic. Here is how you can do that.

// Some cartesian3, typically found by asking an Entity where it is or sampling the 3d scene using your 2d cursor.
const myCartesian3 = new Cesium.Cartesian3(1, 2, 3);

// Converted cartographic. This will be in radians.
const myCartographic = Cesium.Cartographic.fromCartesian(myCartesian3);

// Here is how you can get information into degrees and meters.
const latitude = Cesium.Math.toDegrees(myCartographic.latitude);
const longitude = Cesium.Math.toDegrees(myCartographic.longitude);
const altitude = myCartographic.height;

// Here is how you can do the opposite and turn known lat/lon points into cartesian3.
// Altitude is optional.
const cartesian3 = Cesium.Cartesian3.fromDegrees(longitude, latitude, altitude);
const cartesian3 = Cesium.Cartesian3.fromRadians(longitude, latitude, altitude);

Below is a cheat sheet of some utilities that Nextspace provides within its library to help interact with the Cesium viewer.


As mentioned before getting a 3d position from a 2d cursor has some bugs in Cesium.
Here is a reminder on the utility we provide to help with that.

const my2dPos = new Cesium.Cartesian2(100, 100);
const my3dPos = DrawingUtils.GetAccuratePosition(viewer, my2dPos);

// You can optionally restrict the picking to only allow clicking on Entities instead of the ground.
const my3dPos = DrawingUtils.GetAccuratePosition(viewer, my2dPos, true);

Here is a utility for getting the height of the terrain at a specific position. This will never crash nor return a non-number.
It will include a "error" property within the data if you'd like to know when there was a problem.

const my3dPos = new Cesium.Cartesian3(1, 2, 3);
const data = await DrawingUtils.GetTerrainHeight({
    pos3d: my3dPos,
    viewer
});
const height = data.height;

If you have a polyline and need to know a position x meters along it then use this utility:

const my3dPos = new Cesium.Cartesian3(1, 2, 3);
const myLine = [
    new Cesium.Cartesian3(1, 2, 3),
    new Cesium.Cartesian3(1, 2, 3),
    new Cesium.Cartesian3(1, 2, 3),
    new Cesium.Cartesian3(1, 2, 3)
];
const data = DrawingUtils.PointAcrossPolyline({
    viewer,
    posses: myLine,
    // 50 meters along the line.
    If distance exceeds the length of the line then it will return you the last line position.
    distance: 50
})
const pointOnLine = data.point;

Cesium has a concept of "HeightReference" which means an altitude value can mean different things in different concepts.

This can be quite annoying to program with as an Entity might say "50 meters height" but you'll need to calculate what that means to you.

For example you might want to know the height above the ground or the height above the sea.

let myPos3d = new Cesium.Cartesian3(1, 2, 3);
myPos3d = DrawingUtils.EnsurePosHeight({
    viewer,
    pos3d: myPos3d,
    // In this example we have a point that is positioned above sea level but we want it in relative to ground height.
    desiredHeightRef: Cesium.HeightReference.RELATIVE_TO_GROUND,
    heightRef: Cesium.HeightReference.NONE
})