Drawing rectangles based on camera heading

1. A concise explanation of the problem you’re experiencing.

I’m trying to draw a rectangle based on the heading of the camera. The default rectangle provided by Cesium works perfectly, but only when you’re facing exact North. If the camera is facing a different direction, the rectangle doesn’t highlight the correct area.

I tried to use the rotation attribute of the rectangle, which made it highlight in the correct direction, but this meant that the points selected for the rectangle weren’t necessarily included in the rectangle that was drawn.

The second implementation I tried is found below. It uses a polygon in the back end, but the user won’t be able to tell that it isn’t a rectangle. It uses a callback function that updates the rectangle as the mouse moves. It then translates the first selected point and the mousePoint onto a 2D plane using sinusoidal projection. Then I use some simple 2D maths to calculate the other two corners based on the camera heading and the two 2D points. It then translates the 2D points back into Cesium Cartographic points and plot the polygon using these points.

The problem is that this approach seems to have a few issues. The main issue is that the “rectangle” appears as a parallelogram, even though the 2D points seem to be in a correct rectangle. The second issue seems to be with vertex pathing. In certain directions, the parallelogram appears to be solid, but in other cases it looks like two overlapping triangles.

I’m also drawing points in the callback function to better visualise the vertices.

The code below is compatible with Cesium Sandcastle.

2. A minimal code example. If you’ve found a bug, this helps us reproduce and repair it.

var viewer = new Cesium.Viewer(‘cesiumContainer’);

viewer.terrainProvider = Cesium.createWorldTerrain();

var rectanglePoints = ;

var mousePoint = null;

var rectangleEntity = null;

var currentColor = Cesium.Color.WHITE.withAlpha(0.5);

// function accepts and returns cesium cartographic objects or array of [x,y] points

function sinusoidalProjection(points, reverse) {

let earth_radius = 6371009;

let projectedPoints = ;

if (reverse) {

points.forEach((point) => {

let latitude = point[1]/earth_radius;

let longitude = point[0]/(earth_radius*Math.cos(latitude));

let projectedPoint = Cesium.Cartographic.fromRadians(longitude, latitude);

projectedPoints.push(projectedPoint);

});

} else {

points.forEach((point) => {

let pointX = point.longitude * earth_radius * Math.cos(point.latitude);

let pointY = point.latitude * earth_radius;

let projectedPoint = [pointX, pointY];

projectedPoints.push(projectedPoint);

});

}

return projectedPoints;

}

function addEnt() {

let displayCondition = new Cesium.DistanceDisplayCondition(0, 1200);

// This is the first point

var height = Cesium.Cartographic.fromCartesian(rectanglePoints[0]).height + 0.1;

let heading = (viewer.camera.heading);

let theta = 360-(heading * 180/Math.PI);

testEnt = viewer.entities.add({

polygon: {

hierarchy: new Cesium.CallbackProperty(function() {

let firstCarto = Cesium.Cartographic.fromCartesian(rectanglePoints[0]);

let mouseCarto = Cesium.Cartographic.fromCartesian(mousePoint);

let cartesianPoints = sinusoidalProjection([firstCarto, mouseCarto]);

let c1 = cartesianPoints[0][1]-(Math.tan(theta)*cartesianPoints[0][0]); // c1=y-tan(theta) * x

let c2 = cartesianPoints[1][1]+(cartesianPoints[1][0]/Math.tan(theta)); // c2=y+(x/tan(theta))

let x = (c2-c1)/(Math.tan(theta)+(1/Math.tan(theta)));

let y = Math.tan(theta)*x + c1;

let firstPoint = [x,y];

let c3 = cartesianPoints[0][1]+(cartesianPoints[0][0]/Math.tan(theta)); // c3=y+(x/tan(theta))

let c4 = cartesianPoints[1][1]-(Math.tan(theta)*cartesianPoints[1][0]); // c4=y-tan(theta) * x

x = (c3-c4)/(Math.tan(theta)+(1/Math.tan(theta)));

y = Math.tan(theta)*x+c4;

let secondPoint = [x,y];

console.log(“LOGS”);

console.log("First point " + cartesianPoints[0]);

console.log("Mouse point " + cartesianPoints[1]);

console.log("First found corner " + firstPoint);

console.log("Second found corner " + secondPoint);

let finalCartoPoints = sinusoidalProjection([cartesianPoints[0], cartesianPoints[1], firstPoint, secondPoint], true);

let finalCartesians = ;

finalCartoPoints.forEach((point) => {

let cartesian = Cesium.Cartesian3.fromRadians(point.longitude, point.latitude, 10);

finalCartesians.push(cartesian);

});

finalCartesians.forEach(point => {

viewer.entities.add({

position: point,

point: {

pixelSize: 5,

material: currentColor,

outlineWidth: 2,

},

});

});

console.log(finalCartesians);

return […finalCartesians, mousePoint];

}, false),

material: currentColor,

height: height

}

});

}

new Cesium.ScreenSpaceEventHandler(viewer.canvas).setInputAction(function (event) {

var cartesianPosition = viewer.scene.pickPosition(event.position);

if (rectanglePoints.length === 0) {

rectanglePoints.push(cartesianPosition);

addEnt();

} else {

viewer.entities.remove(rectangleEntity);

rectanglePoints.push(cartesianPosition);

addEnt();

}

}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

new Cesium.ScreenSpaceEventHandler(viewer.canvas).setInputAction(function (event) {

mousePoint = viewer.scene.pickPosition(event.endPosition);

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

``

3. Context. Why do you need to do this? We might know a better way to accomplish your goal.

I’m trying to draw a rectangle based on the heading of the camera.

4. The Cesium version you’re using, your operating system and browser.

Cesium: 1.5

OS: Ubuntu

Browser: Chrome

Is your goal just to draw something that looks like a rectangle in screen space? Couldn’t you just use the mouse’s screen space, 2d coordinate, and draw a polygon that isn’t draped over the surface (with the new coplanar geometry https://github.com/AnalyticalGraphicsInc/cesium/pull/6769) ?

Or is it supposed to look like it’s a perfect rectangle on the surface even when viewed at an angle?

I don’t really understand your first implementation. I want it to look like a perfect rectangle in the screen space (refer to screenshot for example). It should look like this no matter what direction the camera is facing. In the screenshot included, it’s the default Cesium Rectangle entity, but this only looks like a perfect rectangle when the camera is facing directly north.

The polygon should be at a set height that’s equal to the height of the first selected point, so I’m not sure why I’d need to use the coplanar geometry.

Can you please elaborate a bit more on your first solution? I don’t understand what is meant by the mouse’s screen space, or where the 2D coordinates you mentioned are coming from.

The polygon doesn’t need to change to look like a perfect rectangle no matter which angle the user is viewing it from, only from the angle at which they initially create it.

I hope this clears it up a bit,

Thanks for your help Omar

Thanks for explaining more! I see what you mean now.

You’re already getting the 2D coordinates of the mouse on the screen in this line in your code:

new Cesium.ScreenSpaceEventHandler(viewer.canvas).setInputAction(function (event) {

mousePoint = viewer.scene.pickPosition(event.endPosition);

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

``

The function viewer.scene.pickPosition takes a 2D position on the screen and turns it into a 3D position in the world (see https://cesiumjs.org/Cesium/Build/Documentation/Scene.html#pickPosition)

So all you have to do is create your 3 other points based on that first 2D mouse position, convert those to 3D world coordinates, and you’ll get a rectangle that looks correct based on the view it was created in.

I modified the drawing on terrain Sandcastle to show how this would look. Here’s a link to the Sandcastle. The relevant piece of code is:

handler.setInputAction(function(event) {

if (Cesium.defined(floatingPoint)) {

var newPosition = viewer.scene.pickPosition(event.endPosition);

if (Cesium.defined(newPosition)) {

floatingPoint.position.setValue(newPosition);

activeShapePoints = ;

activeShapePoints.push(newPosition);

var pos = new Cesium.Cartesian2(event.endPosition.x + 200, event.endPosition.y);

var pos3D = viewer.scene.pickPosition(pos);

if (Cesium.defined(pos3D)) {

activeShapePoints.push(pos3D);

}

pos = new Cesium.Cartesian2(event.endPosition.x + 200, event.endPosition.y + 100);

pos3D = viewer.scene.pickPosition(pos);

if (Cesium.defined(pos3D)) {

activeShapePoints.push(pos3D);

}

pos = new Cesium.Cartesian2(event.endPosition.x, event.endPosition.y + 100);

pos3D = viewer.scene.pickPosition(pos);

if (Cesium.defined(pos3D)) {

activeShapePoints.push(pos3D);

}

}

}

}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

``

Notice that I offset the points in 2D space to create the 4 corners of the rectangle, and I check that a 3D position exists before adding them to the list of points.

I hope this helped! I’m curious since I haven’t seen this use case before, can you share a bit about the application you’re working on and what this feature is used for?

Awesome, thanks for the help.

I’m working on a drone image data viewer that offers annotation and measurement capabilities. There’s not much practical difference between using a polygon vs making a perfect rectangle, it just didn’t seem like a great UX to offer the user the ability to draw a rectangle, but it would only look good if they drew it while facing the right direction. I’d be happy to show you a demo if you wanted.

I’d love to see! If it’s not public feel free to send me a note at omar@cesium.com.