control/drawing/PolygonDrawingScene.tstypescript
import * as math from "../../math";
import { createEvents, type EventsHandler } from "../../Events";
import type { CoordinatesType, IGeometryStyle } from "../../entity/geometry/Geometry";
import type { IGeoObjectParams } from "../../entity/geoObject/GeoObject";
import type { IPolylineParams } from "../../entity/polyline/Polyline";
import { Entity } from "../../entity/Entity";
import type { IMouseState } from "../../renderer/RendererEvents";
import { LonLat } from "../../LonLat";
import { Line3 } from "../../math/Line3";
import { Object3d } from "../../Object3d";
import { Planet } from "../../scene/Planet";
import { Scene } from "../../scene/Scene";
import { Vec2 } from "../../math/Vec2";
import { Vec3 } from "../../math/Vec3";
import { Vector } from "../../layer/Vector";
import { SHADE_UNLIT } from "../../shadeModeConstants";

type PolygonDrawingSceneEventsList = ["change", "startpoint"];

const POLYGONDRAWINGSCENE_EVENTS: PolygonDrawingSceneEventsList = ["change", "startpoint"];

export interface IPolygonDrawingSceneParams {
    coordinates?: CoordinatesType[];
    cornerStyle?: IGeoObjectParams;
    centerStyle?: IGeoObjectParams;
    outlineStyle?: IPolylineParams;
    fillStyle?: IGeometryStyle;
    name: string;
}

const POINTER_OBJ3D = Object3d.createCylinder(1, 1, 2.0, 20, 1, true, false, 0, -0.5, 0);

export const NUM_SEGMENTS = 300;
export const OUTLINE_ALT = 0.0;

class PolygonDrawingScene extends Scene {
    public events: EventsHandler<PolygonDrawingSceneEventsList>;
    public _planet: Planet | null;
    public _cornerStyle: IGeoObjectParams;
    public _centerStyle: IGeoObjectParams;
    public _outlineStyle: IPolylineParams;
    public _fillStyle: IGeometryStyle;
    protected _initCoordinates: CoordinatesType[];
    protected _pickedCorner: Entity | null;
    protected _pickedCenter: Entity | null;
    protected _startPos: Vec2 | null;
    protected _startClick: Vec2;
    protected _geometryLayer: Vector;
    protected _cornerLayer: Vector;
    protected _centerLayer: Vector;
    protected _outlineLayer: Vector;
    protected _ghostCorner: Entity;
    protected _ghostOutlineLayer: Vector;
    protected _showGhostPointer: boolean;
    protected _isStartPoint: boolean;
    protected _insertCornerIndex: number;
    protected _cornerDblClick = false;

    constructor(options: IPolygonDrawingSceneParams) {
        super(options.name);

        this.events = createEvents(POLYGONDRAWINGSCENE_EVENTS);

        this._planet = null;

        this._initCoordinates = options.coordinates || [];

        this._pickedCorner = null;
        this._pickedCenter = null;

        this._startPos = null;
        this._startClick = new Vec2();

        this._geometryLayer = new Vector();

        //
        // options for vectors
        //
        this._cornerStyle = {
            scale: 0.5,
            tag: "corners",
            color: "rgb(350, 350, 0)",
            object3d: POINTER_OBJ3D,
            ...(options.cornerStyle || {})
        };

        this._centerStyle = {
            scale: 0.4,
            tag: "centers",
            color: "rgb(0, 350, 50)",
            object3d: POINTER_OBJ3D,
            ...(options.centerStyle || {})
        };

        this._outlineStyle = {
            thickness: 4.5,
            color: "rgb(0, 350, 50)",
            ...(options.outlineStyle || {})
        };

        this._fillStyle = {
            fillColor: "rgba(0,146,247,0.2)",
            ...(options.fillStyle || {})
        };

        //
        // outline vectors
        //
        this._cornerLayer = new Vector("corners", {
            pickingScale: 1.2,
            pickingEnabled: true,
            relativeToGround: true,
            shadeMode: SHADE_UNLIT,
            scaleByDistance: [1, 4000000, math.MAX32, 0.005]
        });

        this._centerLayer = new Vector("centers", {
            pickingScale: 1.2,
            pickingEnabled: true,
            relativeToGround: true,
            shadeMode: SHADE_UNLIT,
            scaleByDistance: [1, 4000000, math.MAX32, 0.005]
        });

        this._outlineLayer = new Vector("outline", {
            entities: [
                new Entity({
                    polyline: {
                        path3v: [],
                        ...this._outlineStyle
                    }
                })
            ],
            pickingEnabled: false,
            depthOffset: -10,
            relativeToGround: true
        });

        this._outlineLayer.getEntities()[0].polyline!.altitude = OUTLINE_ALT;

        //
        // Ghost cursor pointer
        //
        this._ghostCorner = new Entity({
            geoObject: this._cornerStyle
        });

        this._ghostOutlineLayer = new Vector("ghost-pointer", {
            pickingEnabled: false,
            depthOffset: -10,
            shadeMode: SHADE_UNLIT,
            relativeToGround: true,
            scaleByDistance: [1, 4000000, math.MAX32, 0.005],
            opacity: 0.5
        });

        this._showGhostPointer = false;

        this._isStartPoint = false;

        this._insertCornerIndex = -1;
    }

    public get geometryType(): string {
        return "POLYGON";
    }

    public getCoordinates(): CoordinatesType[] {
        let corners = this._cornerLayer.getEntities();
        if (corners.length > 0) {
            return corners.map((c: Entity) => {
                let ll = c.getLonLat();
                return [ll.lon, ll.lat, ll.height];
            });
        } else {
            return this._initCoordinates;
        }
    }

    public bindPlanet(planet: Planet) {
        this._planet = planet;
    }

    public override init() {
        this._initEvents();

        this._initGhostLayerPointer();

        if (this._initCoordinates.length) {
            this.setCoordinates(this._initCoordinates);
        }

        this._planet!.addLayer(this._outlineLayer);
        this._planet!.addLayer(this._cornerLayer);
        this._planet!.addLayer(this._centerLayer);

        this.showGhostPointer();
        this.startNewPoint();

        this._geometryLayer.addTo(this._planet!);

        this.events.on("change", this._onChange, this);
        this.renderer!.events.on("forwardpass", this._onForwardpass, this);
    }

    protected _onChange = (e: PolygonDrawingScene) => {
        if (e.geometryType === "POLYGON") {
            let coords = this.getCoordinates();
            let entity = new Entity({
                geometry: {
                    type: e.geometryType,
                    coordinates: [coords],
                    style: this._fillStyle
                }
            });
            this._geometryLayer.clear();
            this._geometryLayer.add(entity);
        }
    };

    override onremove() {
        this.renderer?.events.off("forwardpass", this._onForwardpass);
        this._clearEvents();
        this.hideGhostPointer();
        this.stopNewPoint();
        this.clear();
        this._geometryLayer.remove();
    }

    public clear() {
        this._geometryLayer.clear();

        let corners = this._cornerLayer.getEntities();

        let i = corners.length;
        while (i--) {
            corners[i].remove();
        }

        let centers = this._centerLayer.getEntities();
        i = centers.length;
        while (i--) {
            centers[i].remove();
        }

        let entities = this._outlineLayer.getEntities();
        i = entities.length;
        while (i--) {
            entities[i].polyline!.clear();
            if (i > 0) {
                entities[i].remove();
            }
        }

        this._clearGhostPointer();
    }

    public setCoordinates(coords: CoordinatesType[]) {
        this.clear();
        for (let i = 0; i < coords.length; i++) {
            let ci = coords[i];
            let cart = this._planet!.ellipsoid.lonLatToCartesian(new LonLat(ci[0], ci[1], ci[2]));
            this._appendCart(cart);
        }
        this.events.dispatch(this.events.change, this);
    }

    public stopNewPoint() {
        if (this.renderer) {
            this.renderer.events.off("ldblclick", this._onMouseDblClick);
        }
    }

    public startNewPoint() {
        this.renderer!.events.on("ldblclick", this._onMouseDblClick, this);
    }

    public showGhostPointer() {
        this._showGhostPointer = true;
        this._planet!.addLayer(this._ghostOutlineLayer);
        this._insertCornerIndex = this._cornerLayer.getEntities().length;
    }

    public hideGhostPointer() {
        this._showGhostPointer = false;
        this._ghostOutlineLayer.remove();
        this._insertCornerIndex = -1;
    }

    public setGhostPointerPosition(groundPos: Vec3) {
        if (groundPos) {
            this._ghostCorner.setCartesian3v(groundPos);
            this._updateGhostOutlinePointer(groundPos);
        }
    }

    protected _onCornerMouseEnter = (e: IMouseState) => {
        e.renderer.handler.canvas!.style.cursor = "pointer";
        this.hideGhostPointer();
    };

    _onCornerMouseLeave = (e: IMouseState) => {
        e.renderer.handler.canvas!.style.cursor = "default";
        this.showGhostPointer();
    };

    _onCenterMouseEnter = (e: IMouseState) => {
        e.renderer.handler.canvas!.style.cursor = "pointer";
        this.hideGhostPointer();
    };

    _onCenterMouseLeave = (e: IMouseState) => {
        e.renderer.handler.canvas!.style.cursor = "default";
        if (!(this._pickedCenter || this._pickedCorner)) {
            this.showGhostPointer();
        }
    };

    protected _onLup = (e: IMouseState) => {
        this._planet!.renderer!.controls.navigation?.activate();
        if (this._pickedCorner || this._pickedCenter) {
            this.events.dispatch(this.events.change, this);
            this.setGhostPointerPosition(this._planet!.getCartesianFromPixelTerrain(e)!);
            this.showGhostPointer();
            this._pickedCorner = null;
            this._pickedCenter = null;
        }
    };

    protected _getLdown(e: IMouseState): Entity | null {
        this._planet!.renderer!.controls.navigation?.deactivate();
        this._startClick.set(e.x, e.y);
        let coords = e.pickingObject.getCartesian();
        this._startPos = this._planet!.getPixelFromCartesian(coords);
        return e.pickingObject;
    }

    protected _onCornerLdown = (e: IMouseState) => {
        this._pickedCorner = this._getLdown(e);
    };

    protected _onCenterLdown = (e: IMouseState) => {
        this._pickedCenter = this._getLdown(e);
    };

    protected _onMouseMove = (e: IMouseState) => {
        if (this._pickedCenter) {
            this._moveCenterPoint();
        } else if (this._pickedCorner) {
            this._moveCornerPoint(e.pos!);
        } else {
            this.setGhostPointerPosition(this._planet!.getCartesianFromPixelTerrain(e.pos)!);
        }
    };

    protected _onCornerLdblclick = (e: IMouseState) => {
        this._cornerDblClick = true;
        let coords = this.getCoordinates();
        coords.splice(e.pickingObject.layerIndex, 1);
        this.setCoordinates(coords);
    };

    protected _onMouseDblClick = (e: IMouseState) => {
        if (this._cornerDblClick) {
            this._cornerDblClick = false;
            return;
        }

        if (!this._showGhostPointer) {
            return;
        }

        let cart = this._planet!.getCartesianFromPixelTerrain(e);
        if (cart) {
            this._addNew(cart);
            if (!this._isStartPoint && this._cornerLayer.getEntities().length > 2) {
                this._isStartPoint = true;
                this.events.dispatch(this.events.startpoint, this);
            }
            this.events.dispatch(this.events.change, this);
        }
    };

    protected _initEvents() {
        this._cornerLayer.events.on("ldblclick", this._onCornerLdblclick, this);
        this._cornerLayer.events.on("ldown", this._onCornerLdown, this);
        this._centerLayer.events.on("ldown", this._onCenterLdown, this);

        this.renderer!.events.on("lup", this._onLup, this);
        this.renderer!.events.on("mousemove", this._onMouseMove, this);

        this._cornerLayer.events.on("mouseenter", this._onCornerMouseEnter, this);
        this._cornerLayer.events.on("mouseleave", this._onCornerMouseLeave, this);
        this._centerLayer.events.on("mouseenter", this._onCenterMouseEnter, this);
        this._centerLayer.events.on("mouseleave", this._onCenterMouseLeave, this);
    }

    protected _clearEvents() {
        this._cornerLayer.events.off("ldblclick", this._onCornerLdblclick);
        this._cornerLayer.events.off("ldown", this._onCornerLdown);
        this._centerLayer.events.off("ldown", this._onCenterLdown);

        this.renderer!.events.off("lup", this._onLup);
        this.renderer!.events.off("mousemove", this._onMouseMove);

        this._cornerLayer.events.off("mouseenter", this._onCornerMouseEnter);
        this._cornerLayer.events.off("mouseleave", this._onCornerMouseLeave);
        this._centerLayer.events.off("mouseenter", this._onCenterMouseEnter);
        this._centerLayer.events.off("mouseleave", this._onCenterMouseLeave);
    }

    protected _drawCorners() {
        let corners = this._cornerLayer.getEntities();
        for (let i = 0; i < corners.length; i++) {
            let ai = corners[i];
            this._checkTerrainCollision(ai);
        }
    }

    protected _drawCenters() {
        let centers = this._centerLayer.getEntities();
        for (let i = 0; i < centers.length; i++) {
            let ai = centers[i];
            this._checkTerrainCollision(ai);
        }
    }

    protected _drawGhostCorner() {
        if (this._showGhostPointer) {
            this._checkTerrainCollision(this._ghostCorner);
        }
    }

    protected _onForwardpass = () => {
        this._drawCorners();
        this._drawCenters();
        this._drawGhostCorner();
    };

    protected _checkTerrainCollision(entity: Entity) {
        let _tempTerrPoint = new Vec3();
        let nodes = this._planet!.quadTreeStrategy._renderedNodes;
        for (let j = 0; j < nodes.length; j++) {
            let seg = nodes[j].segment;
            if (seg && seg._extentLonLat.isInside(entity.getLonLat())) {
                seg.getEntityTerrainPoint(entity, _tempTerrPoint);
                entity.setCartesian3v(_tempTerrPoint);
                break;
            }
        }
    }

    protected _moveCenterPoint() {
        let coords = this.getCoordinates(),
            index = this._pickedCenter!.layerIndex + 1,
            ll = this._pickedCenter!.getLonLat();
        let newCorner: CoordinatesType = [ll.lon, ll.lat, ll.height];

        coords.splice(index, 0, newCorner);

        this.setCoordinates(coords);

        this._pickedCenter = null;

        this._pickedCorner = this._cornerLayer.getEntities()[index];
    }

    protected _addNew(cart: Vec3) {
        if (this._insertCornerIndex === -1 || this._cornerLayer.getEntities().length < 2) {
            this._appendCart(cart);
        } else {
            let area = this.getCoordinates(),
                index = this._insertCornerIndex;
            let ll = this._planet!.ellipsoid.cartesianToLonLat(cart);
            let newCorner: CoordinatesType = [ll.lon, ll.lat, ll.height];
            area.splice(index, 0, newCorner);
            this.clear();
            this.setCoordinates(area);
        }
    }

    protected _appendCart(cart: Vec3) {
        let corners = this._cornerLayer.getEntities();

        let segNum = corners.length - 1;
        let prevCorn = corners[segNum];

        let corner = new Entity({
            geoObject: this._cornerStyle
        });

        corner.setCartesian3v(cart);
        corner.addTo(this._cornerLayer);
        this._checkTerrainCollision(corner);

        if (prevCorn) {
            let firstCart = corners[0].getCartesian(),
                prevCart = prevCorn.getCartesian();

            let vecPrev = corner.getCartesian().sub(prevCart),
                vecFirst = corner.getCartesian().sub(firstCart);

            let distPrev = vecPrev.length(),
                distFirst = vecFirst.length();

            vecPrev.normalize();
            vecFirst.normalize();

            let prevPath = [],
                firstPath = [];

            for (let i = 0; i <= NUM_SEGMENTS; i++) {
                let p = vecPrev.scaleTo((i * distPrev) / NUM_SEGMENTS).addA(prevCart);
                prevPath.push(p);

                let f = vecFirst.scaleTo((i * distFirst) / NUM_SEGMENTS).addA(firstCart);
                firstPath.push(f);
            }

            this._outlineLayer.getEntities()[0].polyline!.setPath3v([firstPath]);

            let entity = new Entity({
                polyline: {
                    path3v: [prevPath],
                    ...this._outlineStyle
                }
            });
            entity.polyline!.altitude = OUTLINE_ALT;
            this._outlineLayer.add(entity);

            let centers = this._centerLayer.getEntities();
            let firstCenter = centers[centers.length - 1];

            let prevCenterCart = vecPrev.scaleTo(distPrev * 0.5).addA(prevCart),
                firstCenterCart = vecFirst.scaleTo(distFirst * 0.5).addA(firstCart);

            let center = new Entity({
                geoObject: this._centerStyle
            });
            center.setCartesian3v(prevCenterCart);
            center.addTo(this._centerLayer);
            this._checkTerrainCollision(center);

            //moveToEnd
            firstCenter.remove();
            firstCenter.addTo(this._centerLayer);

            firstCenter.setCartesian3v(firstCenterCart);
        } else {
            let center = new Entity({
                geoObject: this._centerStyle
            });
            center.addTo(this._centerLayer);
        }
    }

    protected _clearGhostPointer() {
        const g = this._ghostOutlineLayer;
        g.getEntities()[0].polyline!.clear();
        g.getEntities()[1].polyline!.clear();
    }

    protected _moveCornerPoint(e: Vec2) {
        let d = new Vec2(e.x, e.y).sub(this._startClick),
            p = this._startPos!.add(d);

        let groundCoords = this._planet!.getCartesianFromPixelTerrain(p);

        if (groundCoords) {
            this._pickedCorner!.setCartesian3v(groundCoords);

            let corners = this._cornerLayer.getEntities();

            if (corners.length) {
                let ind = this._pickedCorner!.layerIndex;
                let size = corners.length;

                let cartPrev = corners[ind === 0 ? size - 1 : ind - 1].getCartesian(),
                    cartNext = corners[(ind + 1) % size].getCartesian();

                let vecPrev = this._pickedCorner!.getCartesian().sub(cartPrev),
                    vecNext = this._pickedCorner!.getCartesian().sub(cartNext);

                let distPrev = vecPrev.length(),
                    distNext = vecNext.length();

                vecPrev.normalize();
                vecNext.normalize();

                let pathPrev = [],
                    pathNext = [];

                for (let i = 0; i <= NUM_SEGMENTS; i++) {
                    let p = vecPrev.scaleTo((i * distPrev) / NUM_SEGMENTS).addA(cartPrev);
                    pathPrev.push(p);

                    let f = vecNext.scaleTo((i * distNext) / NUM_SEGMENTS).addA(cartNext);
                    pathNext.push(f);
                }

                let entities = this._outlineLayer.getEntities();

                let prevPolyline = entities[ind].polyline,
                    nextPolyline = entities[(ind + 1) % size].polyline;

                prevPolyline?.setPath3v([pathPrev]);
                nextPolyline?.setPath3v([pathNext]);

                //
                // Move center points
                let centers = this._centerLayer.getEntities();
                let prevCenter = centers[ind === 0 ? size - 1 : ind - 1],
                    nextCenter = centers[ind];

                let prevCenterCart = vecPrev.scaleTo(distPrev * 0.5).addA(cartPrev),
                    nextCenterCart = vecNext.scaleTo(distNext * 0.5).addA(cartNext);

                prevCenter.setCartesian3v(prevCenterCart);
                this._checkTerrainCollision(prevCenter);

                nextCenter.setCartesian3v(nextCenterCart);
                this._checkTerrainCollision(nextCenter);
            }
        }
    }

    protected _updateGhostOutlinePointer(groundPos: Vec3) {
        let corners = this._cornerLayer.getEntities();
        let size = corners.length;

        if (size > 0) {
            let ind = 0;

            let minDist = math.MAX;

            for (let i = 0; i < size; i++) {
                let ci = corners[i];
                let dist = ci.getCartesian().distance(groundPos);
                if (dist < minDist) {
                    minDist = dist;
                    ind = i;
                }
            }

            let cCurr = corners[ind].getCartesian(),
                cNext = corners[(ind + 1) % size].getCartesian(),
                cPrev = corners[ind === 0 ? size - 1 : ind - 1].getCartesian();

            let nPrev = cPrev.sub(cCurr).normalize(),
                nNext = cNext.sub(cCurr).normalize(),
                nGround = groundPos.sub(cCurr).normalize();

            let midVec = nPrev.add(nNext).normalize();

            let toMid = nGround.cross(midVec),
                up = nPrev.cross(nNext);

            if (toMid.dot(up) > 0) {
                ind--;
                if (ind < 0) {
                    ind = size - 1;
                }
            }

            let temp = new Vec3();

            for (let i = 0; i < size; i++) {
                let side = new Line3(corners[i].getCartesian(), corners[(i + 1) % size].getCartesian());
                let u = side.getNearestDistancePoint(groundPos, temp);
                if (u) {
                    let dist = temp.distance(groundPos);
                    if (dist < minDist) {
                        minDist = dist;
                        ind = i;
                    }
                }
            }

            this._insertCornerIndex = (ind + 1) % size;

            let cartPrev = corners[ind % size].getCartesian(),
                cartNext = corners[(ind + 1) % size].getCartesian();

            let vecPrev = this._ghostCorner.getCartesian().sub(cartPrev),
                vecNext = this._ghostCorner.getCartesian().sub(cartNext);

            let distPrev = vecPrev.length(),
                distNext = vecNext.length();

            vecPrev.normalize();
            vecNext.normalize();

            let pathPrev = [],
                pathNext = [];

            for (let i = 0; i <= NUM_SEGMENTS; i++) {
                let p = vecPrev.scaleTo((i * distPrev) / NUM_SEGMENTS).addA(cartPrev);
                pathPrev.push(p);

                let f = vecNext.scaleTo((i * distNext) / NUM_SEGMENTS).addA(cartNext);
                pathNext.push(f);
            }

            let entities = this._ghostOutlineLayer.getEntities();

            let prevPolyline = entities[0].polyline,
                nextPolyline = entities[1].polyline;

            prevPolyline?.setPath3v([pathPrev]);
            nextPolyline?.setPath3v([pathNext]);
        }
    }

    protected _initGhostLayerPointer() {
        this._ghostOutlineLayer.setEntities([
            new Entity({
                polyline: {
                    path3v: [],
                    ...this._outlineStyle
                }
            }),
            new Entity({
                polyline: {
                    path3v: [],
                    ...this._outlineStyle
                }
            }),
            this._ghostCorner
        ]);

        const g = this._ghostOutlineLayer;
        g.getEntities()[0].polyline!.altitude = g.getEntities()[1].polyline!.altitude = OUTLINE_ALT;
    }
}

export { PolygonDrawingScene };