Cesiumjs – Creating Custom Annotations

About CesiumJS 

CesiumJS is an open-source JavaScript library for creating 3D globes and maps. CesiumJS is an easy-to-integrate and uses the library to create interactive web apps having cross-browser compatibility. This library is used by developers across industries, from aerospace to gaming to smart cities and smart 3d drone data inspection in GIS (Geographic Information System) Mapping systems

Cesium inherits standard WGS84 globe and other recognized models for geospatial visualization and modification.

Some of the supported standard 3D models in CesiumJS include:

  • 3D Tileset
  • Pointcloud
  • KML/KMZ
  • Flight Paths (SRT / GPX / MOV)
  • Ortho Map
  • 360 Panorama

Annotations in CesiumJS

Annotations are a handy way of including additional information about a 3D model. Cesium supports a diverse set of annotation types making the implementation of custom annotations simple. Annotations can be created by creating instances of the entity class in cesium and associating relevant attributes to these instances. Typical annotations recommended in a cesium project are:

  • Pin Annotation
  • Measure Distance Annotation
  • Measure Slope/ Angle Annotation
  • Measure Area Annotation

Pin Annotation

Pin is one of the most basic and popular annotation types that can be used to hold information about a specific point in the map. To create a pin annotation in cesium you can follow the following steps:

-> Create a button or toolbox to select which annotation you want to create.

-> On selecting the Pin annotation from the toolbox in the tool selection logic create an input handler for the click

-> When the mouse has clicked anywhere on the map, the mouse position should be translated to the Cartesian Position.

-> Cartesian position can be passed to the entity class along with other relevant information to create a pin at the location of the mouse click on the map.

Code Sample:

this._clickHandler = new Cesium.ScreenSpaceEventHandler(this._viewer.scene.canvas);
this._clickHandler.setInputAction((click) => {
contextMenuController.setLocation(undefined); // hiding the context menu
let clickCartesian = this.getCartesianPosition(click.position);
if (!clickCartesian) return;


let pinEntity = {
       id: Cesium.createGuid(),
       position: [clickCartesian],
       billboard: {
           image: pinBuilder.fromColor(Cesium.Color.RED, 32).toDataURL(),
           color: new Cesium.Color(1.0, 1.0, 1.0, 0.8),
           verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
           eyeOffset: new Cesium.Cartesian3(0.0, 0.0, -0.2),
           disableDepthTestDistance: Number.POSITIVE_INFINITY,
           scaleByDistance: new Cesium.NearFarScalar(400, 1.0, 4000, 0.0),
       },
       annotationType: "annotationPoint",
       annotation: annotation

   if (annotation.name) {
       entity.label = {
           text: annotation.name,
           style: Cesium.LabelStyle.FILL_AND_OUTLINE,
           fillColor: Cesium.Color.WHITE,
           backgroundColor: Cesium.Color.BLACK,
           showBackground: true,
           scale: 0.6,
           pixelOffset: new Cesium.Cartesian2(0, -40),
           eyeOffset: new Cesium.Cartesian3(0, 0, -0.1),
           disableDepthTestDistance: Number.POSITIVE_INFINITY,
           scaleByDistance: new Cesium.NearFarScalar(400, 1.0, 4000, 0.0),
       };
   }

let entity = this.viewer.entities.add(pinEntity);

getCartesianPosition(_mousePosition) {
   let clickCartesianOnModel;
   if (!this._viewer.scene.pickPositionSupported) {
       console.warn("pickPosition not supported!");
       return;
   }
   let ray = this._viewer.camera.getPickRay(_mousePosition);
   let cartesianDefault = this._viewer.scene.globe.pick(ray, this._viewer.scene);
   let pickedObject = this._viewer.scene.pick(_mousePosition);
   if (pickedObject) {
       //this._viewer.scene.render();
       clickCartesianOnModel = this._viewer.scene.pickPosition(_mousePosition);
   }
   return clickCartesianOnModel || cartesianDefault;
}

Looking for Cesium Development Team?

Share the details of your request and we will provide you with a full-cycle team under one roof.

Get an Estimate

 

Distance/Slope Annotation:

Similar to pin annotation, a mouse click handler can be used to measure the distance/slope between two points. In addition to the pin, implementation adds an extra check for two clicks so that distance and slope can be against the respective points.

Code Sample:

  if (!isMidAction) {
                             
       measureFirstPos = clickCartesian.clone(); //firstclick
   } else {
       // second click
       clickCartesian = this.getCartesianPosition(click.position);
       if (!clickCartesian)
           return; // if the user clicks on the start point before a line is drawn, skip it
       let lineMeasureObj = getLineMeasurementInfo([measureFirstPos, clickCartesian]);
       let distanceString = getDistanceString(lineMeasureObj.distance);

       let rise = parseFloat(lineMeasureObj.rise.toFixed(2)); // rise value.
       let distance = 0.05 * parseFloat(lineMeasureObj.distance); // get 5 % of distance.
       let slope =  lineMeasureObj.slopeDegrees;

let distanceEntity = {
   id: Cesium.createGuid(),
   polyline: {
       positions: [measureFirstPos, clickCartesian],
       width: 12,
       outlineWidth: 1.0,
       material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.RED),
       depthFailMaterial: new Cesium.PolylineArrowMaterialProperty(
           new Cesium.Color(1.0, 0.0, 0.0, 0.3)
       )
   },
   annotationType: "annotationDistance",
//add distance, rise, slope attributes here as additional information
};

let entity = this.viewer.entities.add(distanceEntity);
   }
   isMidAction = !isMidAction; // toggle state

export function getLineMeasurementInfo(points) {
 let returnObj = {};

 if (points instanceof Array === false || points.length < 2) {
   return returnObj;
 }

 var pos1Cartographic = Cesium.Cartographic.fromCartesian(points[0]);
 var pos2Cartographic = Cesium.Cartographic.fromCartesian(points[1]);
 var ellipsoidGeodesic1 = new Cesium.EllipsoidGeodesic(pos1Cartographic, pos2Cartographic);
 var ellipsoidGeodesic2 = new Cesium.EllipsoidGeodesic(pos1Cartographic, pos2Cartographic);
 var firstpointFlat = Cesium.Cartographic.clone(pos1Cartographic, new Cesium.Cartographic());
 var secondpointFlat = Cesium.Cartographic.clone(pos2Cartographic, new Cesium.Cartographic());

 var hDistance = ellipsoidGeodesic1.surfaceDistance;
 var vDistance = pos2Cartographic.height - pos1Cartographic.height;
 var slope = (vDistance / hDistance) * 100;
 var degrees = Cesium.Math.toDegrees(Math.atan(vDistance / hDistance));
 var slopeString = degrees.toFixed(2) + "° (" + slope.toFixed(2) + "%)";

 returnObj.flatPoints = [firstpointFlat, secondpointFlat];
 returnObj.run = hDistance;
 returnObj.rise = Math.abs(vDistance);
 returnObj.slopeDegrees = degrees.toFixed(2);
 returnObj.slopePercentage = slope.toFixed(2);
 returnObj.slopeString = slopeString;
 returnObj.surfaceDistance = ellipsoidGeodesic2.surfaceDistance;
 returnObj.distance = Cesium.Cartesian3.distance(points[0], points[1]);
 returnObj.distanceString = getDistanceString(returnObj.distance);

 return returnObj;
}

function getDistanceString(d) {
 d = Number(d);

 if (d >= 1e3) {
   return (d * 0.001).toFixed(2).toString() + "km";
 } else {
   return d.toFixed(2).toString() + "m";
 }
}

Area Measurement: 

Area measurement calculation is given in the code below the implementation is similar to point and distance with some additional work on calculations.

Code Sample:

import * as Cesium from "cesium";
import * as _ from 'lodash';

export let areaMeasurement = (function () {
 let active = false;
 let multi = true;
 let cesiumViewer = null;
 let noOfPoint = 4;
 let pointsArray = [];
 let addedPolygonEntities = [];
 let addedPointEntities = [];
 let lastPolygonId = null;
 let polygonLocationData = {};
 const polygonIdTemplate = "area_polygon";
 const pointIdTemplate = "point";
 const MIN_POINTS = 3;
 let Viewer = null;
 let clickHandler = {};

 let base = {
   checkInitilization: function () {
     if (!cesiumViewer) {
       throw new Error("Cesium is not initialized");
     }
     if (!cesiumViewer) {
       throw new Error("cesiumViewer is not initialized");
     }
   },
   isInitialized: function () {
     try {
       this.checkInitilization();
       return true;
     } catch (err) {

       return false;
     }
   },
   init: function (viewer) {
     if(!_.isEmpty(viewer)) {
         cesiumViewer = viewer;
         Viewer = viewer;
         // click
         clickHandler = new Cesium.ScreenSpaceEventHandler(cesiumViewer.scene.canvas);
         /**
          * Registering the click events
          */
         let _this = this;
         clickHandler.setInputAction(
             function (e) {
                 if (active) {
                     let cartesian = Viewer.getCartesianPosition(e.position);
                     if (cartesian) {
                         _this.addPoint(cartesian);
                     } else {
                         if(Viewer){
                             Viewer.setMsgStatusBar(`Globe was not picked`);
                         }
                     }
                 }
             }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

         clickHandler.setInputAction(
             function (e) {
                 if (active) {
                     _this.finish();
                     _this.deActivate();
                     _this.setActive(true, Viewer); // start the next one
                 }
             }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
     }
   },
   finish: function () {

     // clearing the corner points
     pointsArray = [];

     let currentPolygonId = this.getCurrentPolygonId();
     // drawing = false;
     this.clearAddedPointEntities();
     if (polygonLocationData[currentPolygonId] && (polygonLocationData[currentPolygonId].pointsArray.length < MIN_POINTS)) {
       delete polygonLocationData[currentPolygonId];
       if(Viewer) {
         Viewer.setMsgStatusBar(`Unable to draw polygon as points must be >= ${MIN_POINTS}`)
       }
       return false;
     } else {
       if (polygonLocationData[lastPolygonId]) {
         polygonLocationData[lastPolygonId].status = "drawn";
       }
       // make the coordinate value static
       this.updateFinalDrawData();
       this.reset();
       this.clearLocalVariables();
       if(!_.isEmpty(clickHandler._inputEvents)) {
           clickHandler._inputEvents = {};
       }
       return true
     }
     // finish the drawing
   },
   updateFinalDrawData: function () {
     let viewer = cesiumViewer;

     let entity = this.getPolygonById(lastPolygonId);
     if (!entity) {
       return false;
     }

     let center = Cesium.BoundingSphere.fromPoints(polygonLocationData[lastPolygonId].pointsArray).center;
     //center = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(center);

     entity.position = new Cesium.ConstantProperty(center);
     entity.polygon.hierarchy = new Cesium.ConstantProperty(polygonLocationData[lastPolygonId].pointsArray);

     let projectName = Viewer.getNearProjectCamera();
     let project = Viewer.getViewerAddedProject(projectName);

     cesiumViewer.entities.removeById(entity.id);
     this.clearPolygonById(lastPolygonId);
     this.clearById(lastPolygonId);

     return true;
   },
   clearDataSources: function (datasources) {
     for (let i in datasources) {
       cesiumViewer.dataSources.remove(datasources[i]);
     }
     datasources = [];
   },
   reset: function () {
     this.clearAll();
     pointsArray.length = 0;
     this.clearLocalVariables();
   },
   getCurrentPolygonId: function () {
     if (lastPolygonId && polygonLocationData[lastPolygonId].status === "drawing") {
       return lastPolygonId;
     } else {
       return polygonIdTemplate + "." + (addedPolygonEntities.length + 1);
     }
   },
   getPolygonById: function (id) {
     this.checkInitilization();
     let flag = false;

     if (!id) {
       return false;
     }

     for (let i in addedPolygonEntities) {
       if (addedPolygonEntities[i] === id) {
         flag = true;
         break;
       }
     }

     if (flag) {
       return cesiumViewer.entities.getById(id);
     }

     return flag;
   },
   clearPolygonById: function (id) {
     this.checkInitilization();
     let flag = false;

     if (!id) {
       return false;
     }

     for (let i in addedPolygonEntities) {
       if (addedPolygonEntities[i] === id) {
         flag = true;
         break;
       }
     }

     if (flag) {
       //removing the polygon
       cesiumViewer.entities.removeById(id);

       //remove polygon from array
       let index = addedPolygonEntities.indexOf(id);
       addedPolygonEntities.splice(index, 1);
       //addedPolygonEntities.pop(id);

       if (lastPolygonId === id) {
         lastPolygonId = null;
       }
     }

     return flag;
   },
   clearById: function (id) {

     this.checkInitilization();
     let flag = false;

     if (!id) {
       return false;
     }

     for (let i in addedPolygonEntities) {
       if (addedPolygonEntities[i] === id) {
         flag = true;
         break;
       }
     }

     if (flag) {
       let arr = [];
       for (let i in addedPointEntities) {
         if (addedPointEntities[i].indexOf(id) !== -1) {
           //we now know that this point is for this polygon
           //removing this point
           cesiumViewer.entities.removeById(addedPointEntities[i]);

           //remove the id from the array
           arr.push(addedPointEntities[i]);
         }
       }

       //remove points id from array
       for (let i in arr) {
         let index = addedPointEntities.indexOf(arr[i]);
         addedPointEntities.splice(index, 1);
       }

       //removing the polygon
       cesiumViewer.entities.removeById(id);

       //remove polygon from array
       let index = addedPolygonEntities.indexOf(id);
       addedPolygonEntities.splice(index, 1);
       delete polygonLocationData[id];

       if (lastPolygonId === id) {
         lastPolygonId = null;
       }
     }

     return flag;

   },
   clearAddedPointEntities: function () {
     this.checkInitilization();

     // remove points
     for (let i in addedPointEntities) {
       cesiumViewer.entities.removeById(addedPointEntities[i]);
       // delete addedPolygonEntities[i];
     }

     addedPointEntities = [];

     return true;
   },
   clearAll: function () {
     this.checkInitilization();
     let flag = false;

     //remove points
     for (let i in addedPointEntities) {
       cesiumViewer.entities.removeById(addedPointEntities[i]);
       // delete addedPolygonEntities[i];
     }
     if (addedPointEntities.length) {
       flag = true;
     }

     //remove polygons
     for (let i in addedPolygonEntities) {
       cesiumViewer.entities.removeById(addedPolygonEntities[i]);
       // delete addedPolygonEntities[i];
     }

     if (addedPolygonEntities.length) {
       flag = true;
     }

     addedPolygonEntities = [];
     addedPointEntities = [];
     polygonLocationData = {};
     lastPolygonId = null;

     return flag;
   },
   clearForceFully: () => {
       //remove points
       for (let i in addedPointEntities) {
           cesiumViewer.entities.removeById(addedPointEntities[i]);
           // delete addedPolygonEntities[i];
       }
       let flag = (addedPointEntities.length > 0);

       //remove polygons
       for (let i in addedPolygonEntities) {
           cesiumViewer.entities.removeById(addedPolygonEntities[i]);
           // delete addedPolygonEntities[i];
       }

       flag = (addedPolygonEntities.length > 0);
       addedPolygonEntities = [];
       addedPointEntities = [];
       polygonLocationData = {};
       lastPolygonId = null;
       return flag;
   },
   addPoint: function (cartesian) {
     if (active) {
       this.checkInitilization();

       if (multi === false && addedPolygonEntities.length && polygonLocationData[lastPolygonId].status === "drawn") {
         this.clearById(lastPolygonId);
         //or that.clearAll();
       }

       let currentPolygonId = this.getCurrentPolygonId();

       this.drawPoint(cartesian, {});

       if (!polygonLocationData[currentPolygonId]) {
         polygonLocationData[currentPolygonId] = {
           pointsArray: [],
           status: "drawing"
         };
       }
       polygonLocationData[currentPolygonId].pointsArray.push(cartesian);

       pointsArray.push(cartesian);

       if (polygonLocationData[currentPolygonId].pointsArray.length === MIN_POINTS) {
         this.drawPolygon({
           outline: true,
           outlineWidth: 3,
           material: Cesium.Color.BLUE.withAlpha(0.5),
           outlineColor: Cesium.Color.BLACK,
           annotationType: "annotationArea",
         });
       }

       if (polygonLocationData[currentPolygonId].pointsArray.length >= MIN_POINTS) {

         let resultObj = this.calculateANDSetArea(this.getPolygonById(lastPolygonId));

         //this.drawLabel(resultObj);
       }

       return true;
     } else {
       return false;
     }

   },
   drawPoint: function (cartesian, obj) {
     this.checkInitilization();
     if (active) {
       obj = obj || {};
       obj.name = obj.name || 'Point';
       obj.id = polygonIdTemplate + "." + (addedPolygonEntities.length + 1) + "_" + pointIdTemplate + "." + (addedPointEntities.length + 1);

       let labelText = addedPointEntities.length + 1;

       labelText = labelText === 0 ? noOfPoint : labelText;

       cesiumViewer.entities.add({
         id: obj.id,
         name: obj.name,
         position: cartesian.clone(),
         point: {
           pixelSize: 4,
           color: Cesium.Color.BLUE,
           outlineColor: Cesium.Color.BLACK,
           outlineWidth: 1
         },
         label: {
           text: labelText + "",
           font: '14pt monospace',
           style: Cesium.LabelStyle.FILL_AND_OUTLINE,
           outlineWidth: 2,
           verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
           pixelOffset: new Cesium.Cartesian2(0, -9)
         },
         description: '<h4>point no ' + (addedPointEntities.length + 1) + '</h4>'
       });

       addedPointEntities.push(obj.id);

       return true;
     } else {
       return false;
     }


   },
   getArea: function (positions, changePosition = true) {

     if(changePosition) {
       positions = positions.positions;
     }

     if (positions instanceof Array === false) {
       return null;
     }

     // "indices" here defines an array, elements of which defines the indice of a vector
     // defining one corner of a triangle. Add up the areas of those triangles to get
     // an approximate area for the polygon

     let indices = Cesium.PolygonPipeline.triangulate(positions, []);
     let area = 0; // In square meters

     for (let i = 0; i < indices.length; i += 3) {
       let vector1 = positions[indices[i]];
       let vector2 = positions[indices[i + 1]];
       let vector3 = positions[indices[i + 2]];

       // These vectors define the sides of a parallelogram (double the size of the triangle)
       let vectorC = Cesium.Cartesian3.subtract(vector2, vector1, new Cesium.Cartesian3());
       let vectorD = Cesium.Cartesian3.subtract(vector3, vector1, new Cesium.Cartesian3());

       // Area of parallelogram is the cross product of the vectors defining its sides
       let areaVector = Cesium.Cartesian3.cross(vectorC, vectorD, new Cesium.Cartesian3());

       // Area of the triangle is just half the area of the parallelogram, add it to the sum.
       area += Cesium.Cartesian3.magnitude(areaVector) / 2.0;
     }

     let areaInHectare = (area / 1e4).toFixed(4);

     let resultObj = {
       areaInHectare: areaInHectare,
       areaInHectareString: areaInHectare + "ha",
       areaInMeter: area,
       areaInMeterString: area.toFixed(4) + ' sqm',
       areaInCM: (area * 10000),
       areaInCMString: (area * 10000).toFixed(4) + ' sq cm'
     }

     if (area >= 1e6) {
       area = area / 1e6;
       resultObj.areaInMeterString = area.toFixed(4) + ' sqkm';
     }

     return resultObj;
   },
   calculateANDSetArea: function (entity) {
     if (!entity || !entity.polygon || !entity.polygon.hierarchy) {
       return null;
     }

     let hierarchy = entity.polygon.hierarchy;
     let positions = hierarchy.getValue();

     return this.getArea(positions);
   },
   getLabelString: (areaSQM, units) => {
     // if no units specified, determine a default based on scale
     if (!units) {
       units = "sqm";
       if (areaSQM < 1) { // < 1sqm
         units = "sqcm";
       }
       if (areaSQM > 1e7) { // > 1000ha
         units = "sqkm";
       }
     }

     switch(units) {
       case "sqkm":
         return ((areaSQM / 1e6).toFixed(2) + " " + units);
       case "ha":
         return ((areaSQM / 1e4).toFixed(2) + " " + units);
       default:
         // no-op, drop through to sqm below
       case "sqm":
         return (areaSQM.toFixed(2) + " " + units);
       case "sqcm":
         return ((areaSQM * 10000).toFixed(2) + " " + units);
     }
   },
   drawPolygon: function (obj) {
     let currentPolygonId = this.getCurrentPolygonId();

     obj = obj || {};
     obj.name = obj.name || 'polygon on surface';
     obj.id = currentPolygonId;
     obj.outline = !!obj.outline;
     obj.outlineColor = obj.outlineColor || Cesium.Color.BLACK;
     obj.outlineWidth = obj.outlineWidth || 3;
     obj.material = obj.material || Cesium.Color.BLUE.withAlpha(0.5);

     if (active) {
       this.checkInitilization();

       cesiumViewer.entities.add({
         id: obj.id,
         name: obj.name,
         positions: new Cesium.CallbackProperty(function () {
             //center = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(center);
           return Cesium.BoundingSphere.fromPoints(polygonLocationData[currentPolygonId].pointsArray).center;
         }, false),
         polygon: {
           hierarchy: new Cesium.CallbackProperty(function () {
             return {positions: polygonLocationData[currentPolygonId].pointsArray};
           }, false),
           material: obj.material,
           outline: obj.outline,
           outlineColor: obj.outlineColor,
           outlineWidth: obj.outlineWidth,
           height: 0,
           fill: true,
           perPositionHeight: true,
         },
         annotationType: "annotationArea",
       });
       lastPolygonId = obj.id;
       addedPolygonEntities.push(obj.id);
     } else {
       return false;
     }
   },
  
   clearLocalVariables: function () {
       pointsArray.length = 0;
       addedPolygonEntities.length = 0;
       addedPointEntities.length = 0;
       lastPolygonId = null;
       polygonLocationData = {};
   }
 };

 base["setActive"] = function (status, viewer) {
   let previousStateIsActive = active;
   active = status;
   if (!previousStateIsActive) {
     base.init(viewer);
   }
 };

 base["getActive"] = function () {
   return active;
 };

 base["setNoOfPoint"] = function (data) {
   noOfPoint = data;
 };

 base["getNoOfPoint"] = function () {
   return noOfPoint;
 };

 base["getLastPolygonId"] = function () {
   return lastPolygonId;
 };

 base["deActivate"] = function () {
   active = false;
   pointsArray.length = 0;
   this.clearForceFully();
   this.clearLocalVariables();
   if(!_.isEmpty(clickHandler._inputEvents)) {
     clickHandler._inputEvents = {};
   }
 };

 return base;

})();

End Result:

Pin Annotations:

Distance/ Slope Annotations:

Area Annotation:

Cesiumjs

Share this article

Leave a comment