1

I'm looking for a Cesium Guru for a little help finding what I need. I'm new to Cesium but I've been working with the tutorials and some existing code that I've inherited.

In my Cesium app, I enter my address and the view zooms in to my street. Yay! Then I zoom in closer, so I can draw a polygon around my house. The existing code does this very well. However when I zoom out and then zoom in again, my polygon does not stay true to the Lat-Lon position of my house.

Does Cesium contain a utility to scale pixels to lat-lon coordinates or do I need to use something like distanceToBoundingSphere(boundingSphere) and calculate it myself? I only want the x,y coordinates; I don't care about height at all.

I have been looking around in the demos and tutorials and so far haven't found what I think I'm looking for. Maybe I have found something close but I don't know enough yet to know whether I've found it or not. Help!

============================ CODE ==================================

Collecting the positions for the polygon:

A singleClick only captures the coordinates for that point and draws a polyline as user drags mouse to a new point. Hence, a bunch of polylines and a collection of coordinates for each point.

    positionHandler.setInputAction(function (click) {
        cartesian = scene.camera.pickEllipsoid(click.position, ellipsoid);
        if (cartesian) {
            var setCartographic = ellipsoid.cartesianToCartographic(cartesian);
            asset.latlonalt.push(
                Cesium.Math.toDegrees(setCartographic.latitude).toFixed(15),
                Cesium.Math.toDegrees(setCartographic.longitude).toFixed(15),
                Cesium.Math.toDegrees(setCartographic.height).toFixed(15)
            );
            lla.push(Cesium.Math.toDegrees(setCartographic.longitude), Cesium.Math.toDegrees(setCartographic.latitude));
            if (lla.length >= 4) {
                self.loggingMessage((lla.length / 2) + ' Points Added');
            }
            Cesium.sampleTerrain(terrainProvider, 11, [cartographic])
                .then(function (updatedPositions) {
                    asset.latlonalt[2] = updatedPositions[0].height;
                    stage = 1;
                });
        }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

A doubleClick then takes the coordinates captured in the singleClick function and calls self.createAsset('add', asset) to create the polygon.

    positionHandler.setInputAction(function (doubleClick){
        if (asset.shape == 'Polygon') {
            var len = asset.latlonalt.length;
            if(len > 9) {
                asset.rad = (len / 3);
                console.log("Creating Asset");
                self.loggingMessage("Creating Asset");
                socket.emit('newElement', asset.cType, asset);
                self.createAsset('add', asset);
                viewer.entities.remove(entity);
                viewer.entities.remove(newCircle);
                viewer.entities.remove(newPolygon);
                viewer.entities.remove(newOutline);
                positionHandler = positionHandler && positionHandler.destroy();
            }else{
                console.log('3+ Positions Required');
                loggingMessage('3+ Positions Required.');
            }
        }
    }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)

Creating the polygon:

            var newPolygon = viewer.entities.add({
                name : asset.id,
                polygon : {
                    hierarchy : Cesium.Cartesian3.fromDegreesArray(vertices),
                    material : rgba[0],
                    outline : true,
                    outlineColor : rgba[1]
                }
            });
            var newLabel = viewer.entities.add({
                position: Cesium.Cartesian3.fromDegrees(asset.latlonalt[1], asset.latlonalt[0], 1000),
                name: asset.id,
                label: {
                    text: asset.name,
                    font : '16px Helvetica'
                }
            });
            var newPoint = viewer.entities.add({
                position: Cesium.Cartesian3.fromDegrees(asset.latlonalt[1], asset.latlonalt[0], 0),
                name: asset.id,
                point : {
                pixelSize : 5,
                    color : Cesium.Color.RED,
                    outlineColor : Cesium.Color.RED,
                    outlineWidth : 2
                }
            });
            self.currentGeometry[asset.id] = {shape: newPolygon, label: newLabel, point: newPoint};

It looks like we're using Terrain (I think).

Terrain Image

Which numbers should I be interested in:

enter image description here

When the coordinates are collected, only the first z is not zeros:

enter image description here

I take that value and populated the other z values:

enter image description here

Now that I have added the z values something is erroring out in the createAsset method. I need to track that problem down to see the results. Right now, it looks like this:

enter image description here

REALLY BIG and the outlines are not getting removed.

Patricia
  • 5,019
  • 14
  • 72
  • 152
  • 1
    Welcome to Cesium, Miss Lucy. Does your app use Terrain, by chance? If so, you may be seeing your house at its proper altitude, but drawing a polygon at height zero (WGS84 reference altitude). Such "underground polygons" often appear to drift as the camera moves. – emackey Mar 11 '16 at 02:30
  • Exactly! I can move the camera and zoom in or out until the polygon matches my house and when I move the earth, the polygon shifts away from my house. If by Terrain you mean trees, cars, grass and other stuff, then yes, it must be using Terrain. OK. I'm going to look into this more. Perhaps you can give me an answer and I'll give you the points. :-) – Patricia Mar 11 '16 at 15:56
  • Wait-a-minute...the code only takes into consideration the x,y coordinates. Are you saying that by simply adding the z coordinate my polygon will stay with my house? – Patricia Mar 11 '16 at 15:59
  • By "using terrain" I mean that if you zoom close to some mountains, and tilt the camera towards the horizon, you see mountains sticking up vertically, not pictures of mountains laying flat on the WGS84 ellipsoid. If the former (3D mountains), then yes, terrain is in use, and you must position polygons at non-zero heights even in flat regions. I see you added a call to `sampleTerrain`, did this fix things? – emackey Mar 11 '16 at 21:14
  • I believe we are using terrain. Best I could do was the screen cap. No, I did not add the sampleTerrrain (it was already there). But I am beginning to see what might be the issue. I don't know why he's using the sampleTerrain in that method (first code block) when it only collects the coordinates. Even though the updatedPositions[0].height is correct in the sampleTerrain method, the z values do not appear to get populated. Maybe we should be using the sampleTerrain in the code that actually creates the polygon (third code block). I populate the z values on the vertices array. – Patricia Mar 11 '16 at 23:24
  • See updates in question. I'm stepping through this code and it appears to be a bit convoluted. Can you take a look at the x,y,z numbers and help me figure out the correct logic flow. Except for the fact that the z values are not getting stored in the asset.latlonalt array, the x and y values look good. But even populating the z values in the vertices array the polygon isn't working correctly. I'm still looking at sampleTerrain. Maybe it's just not in the right place. – Patricia Mar 11 '16 at 23:25

1 Answers1

4

There's a lot going on for one question, but I'll try to address the key parts of this.

First, the Cartesian3 class itself. Internally, after construction, this class contains x, y, z members containing values like 314923.1. You should probably think of these as black-box, opaque values. In fact they represent a Cartesian position, in meters, from the center of the planet, which is needed by the rendering engine but not normally useful to human cartographers. The key thing to understand is that z will always be populated with a real value, and this does NOT imply that height was taken into account or not when creating the value.

There is a separate class Cartographic that contains the familiar Longitude (Radians), Latitude (Radians), and Altitude (in meters) values. Generally these must be converted to Cartesian3 before being handed off to the rendering engine. This conversion requires knowledge of the Ellipsoid (which defaults to the WGS84 ellipsoid), and as such, a zero-altitude value indicates a point sitting on that ellipsoid (typically meaning the point is underground when terrain is turned on).

An assortment of helper functions take common values (such as lon/lat in degrees) and convert to either of these two formats. Some of these helper functions leave off the altitude parameter, others include it. The documentation for these two classes enumerates these helpers.

Getting the exact lon/lat from a mouse click is simple when terrain is turned off, but more complex when it's on, due to the use of the perspective 3D camera. Without terrain, you can just call scene.camera.pickEllipsoid as you've done in the first code sample above, and get the exact spot. But when terrain is on, even a click on a flat plain will calculate the wrong coordinates, finding an underground spot below and behind the terrain you're looking at.

A casual search doesn't turn up the correct code for this, but the gold standard appears to be baked into the existing camera controller here. It looks like this:

    var depthIntersection;
    if (scene.pickPositionSupported) {
        depthIntersection = scene.pickPosition(mousePosition, scratchDepthIntersection);
    }

    var ray = camera.getPickRay(mousePosition, pickGlobeScratchRay);
    var rayIntersection = globe.pick(ray, scene, scratchRayIntersection);

    var pickDistance = defined(depthIntersection) ?
        Cartesian3.distance(depthIntersection, camera.positionWC) :
        Number.POSITIVE_INFINITY;
    var rayDistance = defined(rayIntersection) ?
        Cartesian3.distance(rayIntersection, camera.positionWC) :
        Number.POSITIVE_INFINITY;

    if (pickDistance < rayDistance) {
        return Cartesian3.clone(depthIntersection, result);
    }

    return Cartesian3.clone(rayIntersection, result);

This code is attempting a two-pronged approach: It tries to pick the ellipsoid, as before, and is also attempts to pick from the "depth buffer", which is part of the 3D graphics system that allows Cesium to inspect how far polygons were from the camera when rendered. The two results are compared, and whichever is closer to the camera is declared the winner. This avoids the need for a sampleTerrain call at all, because the mouse location has been used to directly pick a Cartesian point in space where a polygon was rendered (which is likely terrain, but could even be the top of a building, etc).

In your next code block, you use asset.latlonalt to populate the lon and the lat, but then you have alt hard-coded to 0 or 1000 rather than coming from the same data structure. This may be where the altitude information is getting lost, if it was there to begin with (which it is not, if you're just picking the ellipsoid itself, although it may be being added by sampleTerrain later. Beware that sampleTerrain is asynchronous due to terrain tiles being loaded from the server). If you decide to try the depth-picking technique, that will yield Cartesian3 directly, so you won't have to worry about converting it or preserving altitude etc.

One last comment, recent versions of Cesium do support a GroundPrimitive that allows polygons to drape on terrain. This won't save you from the need to "pick" the right lon/lat to begin with (taking perspective terrain into account), but will allow a polygon to lay on a lumpy surface and not stick through.

Community
  • 1
  • 1
emackey
  • 11,818
  • 2
  • 38
  • 58
  • Great info!!! I don't want the polygon to drape on the terrain because I am simply trying to define a perimeter (kind of like an invisible fence for pets). OK. I will dive into the info you provided and 'hopefully' get my polygon to stick to the appropriate positions while zooming and moving the camera. Thank you. :-) – Patricia Mar 14 '16 at 18:50
  • Hi Miss Lucy, I saw your follow-on question, but haven't had time to think through the full response to it. Actually it looks like it was just deleted. Anyway you can get a "globe" reference from `viewer.scene.globe`, and you are correct that the altitude is included in your Cartesian3. – emackey Mar 21 '16 at 18:05
  • Hi, emackey! Yes, I just deleted it, because I thought there may be too much in that question as well. I am starting to get the hang of it but I still don't know how to get the defined (i.e., Cesium.defined(depthIntersection) and Cesium.defined(rayIntersection)) and how to get the scene.pickPositionSupported set to true. So, I'm posting another question that is much smaller. I'll post you when it's up there. – Patricia Mar 21 '16 at 18:12
  • 1
    Looking forward to more questions :) In the meantime, `scene.pickPositionSupported` reflects a browser capability (WebGL's Fragment Depth extension being available), which I think is not available in IE 11, but is available on most graphics hardware in Chrome, Firefox, and Edge. – emackey Mar 21 '16 at 18:23