Source: modules/draw/DrawWall.js

import * as Cesium from "cesium";

/**
 * DrawWall class for drawing a polygon in a Cesium viewer.
 * @class DrawWall
 * @param {Cesium.Viewer} viewer - The Cesium viewer instance.
 * @param {Object} [options] - Options for the drawing tool.
 * @param {Cesium.Color} [options.color] - The color of the polygon.
 * @param {boolean} [options.clampToGround] - Whether to clamp the polygon to the ground.
 * @param {number} [options.gap] - The gap between points in meters.
 * @param {number} [options.height] - The height of the wall in meters.
 * @example
 * const drawWall = new DrawWall(viewer, { color: Cesium.Color.RED });
 * drawWall.on();
 * // To disable the drawing tool and clear entities
 * drawWall.off();
 */
export class DrawWall {
    constructor(viewer, options = {}) {
        this.viewer = viewer;
        this.scene = viewer.scene;
        this.handler = new Cesium.ScreenSpaceEventHandler();
        this.color = options.color || Cesium.Color.LIGHTGRAY;
        this.gap = options.gap || 30;
        this.wallHeight = options.wallHeight || 30;
        this.clampToGround = options.clampToGround || false;
        this.status = false;
        this.cartesians = undefined;
        this.endCartesian = undefined;
        this.polygonEntities = [];
        this.polylineEntity = undefined;
        this.pointEntities = [];
    }

    /**
     * Enables the polygon drawing tool.
     * Left click to add points, right click to finish.
     * @function
     * @type {boolean} Continue - Whether to continue measuring after the first click.
     * @returns {void}
     */
    on = (isContinue = false) => {
        this.scene.canvas.style.cursor = "crosshair";
        const viewer = this.viewer;
        const scene = viewer.scene;
        const handler = this.handler;

        const mouseLeftClickHandler = (event) => {
            if (!this.status) {
                this.status = true;

                // Clear previous entities
                this.recentPositions = this.cartesians.slice();
                if (this.recentPositions && this.recentPositions.length > 0) {
                    this.recentPositions.push(this.recentPositions[0]);
                }

                this.clearEntities(viewer);
                this.clearCartesians();
                scene.screenSpaceCameraController.enableRotate = true;
                scene.screenSpaceCameraController.enableTranslate = true;
                scene.screenSpaceCameraController.enableZoom = true;
                scene.screenSpaceCameraController.enableTilt = true;
                scene.screenSpaceCameraController.enableLook = true;
            }

            this.pickedObject = scene.pick(event.position);
            let pickedEllipsoidPosition;
            if (scene.pickPositionSupported) {
                pickedEllipsoidPosition = viewer.scene.pickPosition(
                    event.position);
            }
            if (!pickedEllipsoidPosition) {
                pickedEllipsoidPosition = viewer.camera.pickEllipsoid(
                    event.position,
                    scene.globe.ellipsoid);
                const cartographic = Cesium.Cartographic.fromCartesian(
                    pickedEllipsoidPosition);
                const height = viewer.scene.globe.getHeight(
                    Cesium.Cartographic.fromRadians(cartographic.longitude,
                        cartographic.latitude, 0));
                pickedEllipsoidPosition = Cesium.Cartesian3.fromRadians(
                    cartographic.longitude, cartographic.latitude, height);
            }

            this.cartesians.push(pickedEllipsoidPosition);
            this.endCartesian = pickedEllipsoidPosition;

            const pointEntity = viewer.entities.add({
                position: pickedEllipsoidPosition, point: {
                    show: false,
                    color: this.color,
                    pixelSize: 4,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
            });
            this.pointEntities.push(pointEntity);

            if (this.cartesians.length <= 1) {
                this.polylineEntity = viewer.entities.add({
                    polyline: {
                        positions: new Cesium.CallbackProperty(() => {
                            const cartesianPositions = this.cartesians.slice();
                            cartesianPositions.push(this.endCartesian);
                            // cartesianPositions.push(this.cartesians[0]);
                            return cartesianPositions;
                        }, false),
                        width: 3,
                        depthFailMaterial: this.color,
                        material: this.color.withAlpha(0.8),
                        clampToGround: this.clampToGround,
                    },
                });
            }
        };

        const mouseMoveHandler = (moveEvent) => {
            if (!this.status) {
                return;
            }
            // pickedObject = scene.pick(moveEvent.endPosition);
            let pickedEllipsoidPosition;
            if (scene.pickPositionSupported) {
                pickedEllipsoidPosition = viewer.scene.pickPosition(
                    moveEvent.endPosition);
            }
            if (!pickedEllipsoidPosition) {
                pickedEllipsoidPosition = viewer.camera.pickEllipsoid(
                    moveEvent.endPosition, scene.globe.ellipsoid);
                const cartographic = Cesium.Cartographic.fromCartesian(
                    pickedEllipsoidPosition);
                const height = viewer.scene.globe.getHeight(
                    Cesium.Cartographic.fromRadians(cartographic.longitude,
                        cartographic.latitude, 0));
                pickedEllipsoidPosition = Cesium.Cartesian3.fromRadians(
                    cartographic.longitude, cartographic.latitude, height);
            }
            this.endCartesian = pickedEllipsoidPosition;
        };

        const mouseRightClickHandler = (event) => {
            if (!this.status) {
                return;
            }

            this.status = false;

            this.pickedObject = scene.pick(event.position);
            let pickedEllipsoidPosition;
            if (scene.pickPositionSupported) {
                pickedEllipsoidPosition = viewer.scene.pickPosition(
                    event.position);
            }
            if (!pickedEllipsoidPosition) {
                pickedEllipsoidPosition = viewer.camera.pickEllipsoid(
                    event.position,
                    scene.globe.ellipsoid);
                const cartographic = Cesium.Cartographic.fromCartesian(
                    pickedEllipsoidPosition);
                const height = viewer.scene.globe.getHeight(
                    Cesium.Cartographic.fromRadians(cartographic.longitude,
                        cartographic.latitude, 0));
                pickedEllipsoidPosition = Cesium.Cartesian3.fromRadians(
                    cartographic.longitude, cartographic.latitude, height);
            }

            this.cartesians.push(pickedEllipsoidPosition);

            const area = this.calculateArea(this.cartesians);

            const pointEntity = viewer.entities.add({
                position: pickedEllipsoidPosition, point: {
                    show: false,
                    color: this.color,
                    pixelSize: 4,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
            });
            this.pointEntities.push(pointEntity);

            let cartesianPositions = this.cartesians.slice();

            // separate the positions by gap when clampToGround is true
            if (this.clampToGround) {
                // separate the positions by gap
                const separatedPositions = [];
                for (let i = 0; i < cartesianPositions.length - 1; i++) {
                    const start = cartesianPositions[i];
                    const end = cartesianPositions[i + 1];
                    const distance = Cesium.Cartesian3.distance(start, end);
                    const numPoints = Math.floor(distance / this.gap) + 1;
                    for (let j = 0; j <= numPoints; j++) {
                        let position = Cesium.Cartesian3.lerp(start, end,
                            j / numPoints,
                            new Cesium.Cartesian3());
                        const cartographic = Cesium.Cartographic.fromCartesian(
                            position);
                        const height = viewer.scene.globe.getHeight(
                            Cesium.Cartographic.fromRadians(
                                cartographic.longitude,
                                cartographic.latitude, 0));
                        position = Cesium.Cartesian3.fromRadians(
                            cartographic.longitude,
                            cartographic.latitude, height);
                        separatedPositions.push(position);
                    }
                }
                cartesianPositions = separatedPositions;
            }

            const wallPositions = cartesianPositions.map(cartesian => {
                const cartographic = Cesium.Cartographic.fromCartesian(
                    cartesian);
                const height = viewer.scene.globe.getHeight(
                    Cesium.Cartographic.fromRadians(cartographic.longitude,
                        cartographic.latitude, 0));
                return Cesium.Cartesian3.fromRadians(cartographic.longitude,
                    cartographic.latitude, height + this.wallHeight);
            });

            const wallPolylinePositions = wallPositions.slice();
            wallPolylinePositions.push(
                cartesianPositions[cartesianPositions.length - 1]);

            const wallPolylineEntity = viewer.entities.add({
                polyline: {
                    positions: wallPolylinePositions,
                    width: 3,
                    depthFailMaterial: this.color,
                    material: this.color.withAlpha(0.8),
                },
            });
            this.polygonEntities.push(wallPolylineEntity);

            let floorPolylinePositions = [];
            floorPolylinePositions.push(wallPolylinePositions[0]);
            floorPolylinePositions = floorPolylinePositions.concat(
                cartesianPositions.slice());
            const floorPolylineEntity = viewer.entities.add({
                polyline: {
                    positions: floorPolylinePositions.slice(),
                    width: 3,
                    depthFailMaterial: this.color,
                    material: this.color.withAlpha(0.8),
                },
            });
            this.polygonEntities.push(floorPolylineEntity);
            this.viewer.entities.remove(this.polylineEntity);

            for (let i = 0; i < wallPositions.length - 1; i++) {
                const floorA = cartesianPositions[i];
                const floorB = cartesianPositions[i + 1];
                const wallA = wallPositions[i];
                const wallB = wallPositions[i + 1];
                const resultPositions = [floorA, floorB, wallB, wallA];
                resultPositions.push(floorA);
                const wallHierarchy = new Cesium.PolygonHierarchy(
                    resultPositions);

                const wallEntity = viewer.entities.add({
                    polygon: {
                        hierarchy: wallHierarchy,
                        material: this.color.withAlpha(0.5),
                        perPositionHeight: true,
                    },
                });
                this.polygonEntities.push(wallEntity);
                // break;
            }

            scene.screenSpaceCameraController.enableRotate = true;
            scene.screenSpaceCameraController.enableTranslate = true;
            scene.screenSpaceCameraController.enableZoom = true;
            scene.screenSpaceCameraController.enableTilt = true;
            scene.screenSpaceCameraController.enableLook = true;
        };
        handler.setInputAction(mouseLeftClickHandler,
            Cesium.ScreenSpaceEventType.LEFT_CLICK);
        handler.setInputAction(mouseMoveHandler,
            Cesium.ScreenSpaceEventType.MOUSE_MOVE);
        handler.setInputAction(mouseRightClickHandler,
            Cesium.ScreenSpaceEventType.RIGHT_CLICK);
        handler.setInputAction(mouseRightClickHandler,
            Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
    };

    /**
     * Disables the polygon drawing tool. and clears the entities.
     * @function
     * @returns {void}
     */
    off = () => {
        this.scene.canvas.style.cursor = "default";
        this.status = false;
        this.clearEntities();
        const handler = this.handler;
        if (handler) {
            handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
            handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
            handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
            handler.removeInputAction(
                Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
        }
    };

    clearEntities = () => {
        this.viewer.entities.remove(this.polylineEntity);
        // this.viewer.entities.remove(this.polygonEntity);
        this.polygonEntities.forEach(entity => {
            this.viewer.entities.remove(entity);
        });
        this.pointEntities.forEach(entity => {
            this.viewer.entities.remove(entity);
        });
        this.polylineEntity = undefined;
        this.polygonEntities = [];
        this.pointEntities = [];
        this.cartesians = [];
        this.endCartesian = undefined;
    };

    clearCartesians = () => {
        this.cartesians = [];
        this.endCartesian = undefined;
    };

    calculateArea = (cartesians) => {
        const positions = cartesians;
        const indices = Cesium.PolygonPipeline.triangulate(positions, []);
        let area = 0;
        for (let i = 0; i < indices.length; i += 3) {
            const vector1 = positions[indices[i]];
            const vector2 = positions[indices[i + 1]];
            const vector3 = positions[indices[i + 2]];
            const vectorC = Cesium.Cartesian3.subtract(vector2, vector1,
                new Cesium.Cartesian3());
            const vectorD = Cesium.Cartesian3.subtract(vector3, vector1,
                new Cesium.Cartesian3());
            const areaVector = Cesium.Cartesian3.cross(vectorC, vectorD,
                new Cesium.Cartesian3());
            area += Cesium.Cartesian3.magnitude(areaVector) / 2.0;
        }

        if (area > 100000) {
            area = area / 1000;
            return area.toFixed(3) + " ㎢";
        } else {
            return area.toFixed(3) + " ㎡";
        }
    };

}