control/ruler/RulerScene.tstypescript
import * as math from "../../math";
import { Entity } from "../../entity/Entity";
import { createEvents } from "../../Events";
import type { EventsHandler } from "../../Events";
import { LonLat } from "../../LonLat";
import { Object3d } from "../../Object3d";
import { Planet } from "../../scene/Planet";
import { Scene } from "../../scene/Scene";
import { Vector } from "../../layer/Vector";
import { Vec2 } from "../../math/Vec2";
import { Vec3 } from "../../math/Vec3";
import type { IMouseState } from "../../renderer/RendererEvents";
import { Ellipsoid } from "../../ellipsoid/Ellipsoid";
import type { ILabelParams } from "../../entity/label/Label";
import { SHADE_UNLIT } from "../../shadeModeConstants";

const OUTLINE_COUNT = 120;

export interface IRulerSceneParams {
    name?: string;
    ignoreTerrain?: boolean;
    planet?: Planet;
}

export const distanceFormat = (v: number): string => {
    if (v > 1000) {
        return `${(v / 1000).toFixed(1)} km`;
    } else if (v > 9) {
        return `${Math.round(v)} m`;
    } else {
        return `${v.toFixed(1)} m`;
    }
};

let obj3d = Object3d.createCylinder(0.33, 0, 1.0, 20, 1, true, false, 0, 0, 0);

const LABEL_OPTIONS: ILabelParams = {
    text: "",
    size: 12,
    color: "rgba(255,255,255,1.0)",
    outlineColor: "rgba(0,0,0,1)",
    outline: 2,
    align: "center",
    offset: [0, 20]
};

const RULER_CORNER_OPTIONS = {
    scale: 1,
    instanced: true,
    tag: "ruler",
    color: "rgb(0,205,0)",
    object3d: obj3d
};

type RulerSceneEventsList = [
    "add",
    "remove",
    "mousemove",
    "mouseenter",
    "mouseleave",
    "lclick",
    "rclick",
    "mclick",
    "ldblclick",
    "rdblclick",
    "mdblclick",
    "lup",
    "rup",
    "mup",
    "ldown",
    "rdown",
    "mdown",
    "lhold",
    "rhold",
    "mhold",
    "mousewheel",
    "touchmove",
    "touchstart",
    "touchend",
    "doubletouch",
    "touchleave",
    "touchenter"
];

class RulerScene extends Scene {
    public events: EventsHandler<RulerSceneEventsList>;
    protected _ignoreTerrain: boolean;
    protected _planet: Planet | null;
    protected _startLonLat: LonLat | null;
    protected _preventClick: boolean;
    protected _stopDrawing: boolean;
    protected _pickedCorner: Entity | null;
    protected _startPos: Vec2 | null;
    protected _startClick: Vec2;
    protected _heading: number;
    protected _trackLayer: Vector;
    protected _labelLayer: Vector;
    protected _cornersLayer: Vector;
    protected _cornerEntity: Entity[];
    protected _propsLabel: Entity;
    protected _trackEntity: Entity;
    protected _anchorLonLat: LonLat | null;
    protected _timeout: any;

    constructor(options: IRulerSceneParams = {}) {
        super(options.name);

        this.events = createEvents(RULERSCENE_EVENTS);

        this._ignoreTerrain = options.ignoreTerrain != undefined ? options.ignoreTerrain : true;

        this._planet = options.planet || null;

        this._startLonLat = null;
        this._preventClick = false;
        this._stopDrawing = false;

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

        this._anchorLonLat = null;

        this._heading = 0;

        this._trackLayer = new Vector("track", {
            entities: [],
            pickingEnabled: false,
            depthOffset: -5.0,
            relativeToGround: true,
            hideInLayerSwitcher: true
        });

        this._labelLayer = new Vector("ruler-label", {
            entities: [],
            pickingEnabled: false,
            depthOffset: -5.0,
            relativeToGround: true,
            hideInLayerSwitcher: true
        });

        this._cornersLayer = new Vector("corners", {
            entities: [],
            pickingEnabled: true,
            hideInLayerSwitcher: true,
            scaleByDistance: [1, 5000, math.MAX32, 0.023],
            pickingScale: 2,
            shadeMode: SHADE_UNLIT
        });

        this._propsLabel = new Entity({
            name: "propsLabel",
            label: LABEL_OPTIONS
        });

        this._trackEntity = new Entity({
            polyline: {
                path3v: [],
                thickness: 4.8,
                color: ["rgb(255,131,0)"],
                isClosed: false
            }
        });

        this._trackEntity.polyline!.altitude = 0.01;

        this._cornerEntity = [
            new Entity({
                geoObject: RULER_CORNER_OPTIONS,
                properties: {
                    name: "start"
                }
            }),
            new Entity({
                geoObject: RULER_CORNER_OPTIONS,
                properties: {
                    name: "end"
                }
            })
        ];
    }

    public set ignoreTerrain(v: boolean) {
        this._ignoreTerrain = v;
        if (v) {
            //...redraw line
        }
    }

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

    protected _createCorners() {
        this._cornersLayer.addEntities(this._cornerEntity);
    }

    public override init() {
        this._createCorners();
        this._trackLayer.addEntities([this._trackEntity]);
        this._labelLayer.addEntities([this._propsLabel]);
        this._planet!.addLayer(this._labelLayer);
        this._planet!.addLayer(this._trackLayer);
        this._planet!.addLayer(this._cornersLayer);
        this._activate();
    }

    public override onremove() {
        this._deactivate();
    }

    public _activate() {
        this._propsLabel.label!.setVisibility(false);
        this.setVisibility(false);

        this.renderer!.events.on("lclick", this._onLclick, this);
        this.renderer!.events.on("mousemove", this._onMouseMove, this);
        this.renderer!.events.on("ldblclick", this._onLdblclick, this);
        this.renderer!.events.on("lup", this._onLUp, this);
        this.renderer!.events.on("forwardpass", this._onForwardpass, this);

        this._cornersLayer.events.on("mouseenter", this._onCornerEnter, this);
        this._cornersLayer.events.on("mouseleave", this._onCornerLeave, this);
        this._cornersLayer.events.on("ldown", this._onCornerLdown, this);
        this._cornersLayer.events.on("lup", this._onCornerLup, this);
    }

    protected _deactivate() {
        this._startLonLat = null;
        this._preventClick = false;
        this._stopDrawing = false;
        this._pickedCorner = null;
        // this._trackLayer.remove();
        // this._cornersLayer.remove();

        this.renderer!.events.off("lclick", this._onLclick);
        this.renderer!.events.off("mousemove", this._onMouseMove);
        this.renderer!.events.off("lup", this._onLUp);
        this.renderer!.events.off("forwardpass", this._onForwardpass);

        this._cornersLayer.events.off("mouseenter", this._onCornerEnter);
        this._cornersLayer.events.off("mouseleave", this._onCornerLeave);
        this._cornersLayer.events.off("ldown", this._onCornerLdown);
        this._cornersLayer.events.off("lup", this._onCornerLup);

        this.clear();
    }

    protected _onCornerLdown = (e: IMouseState) => {
        if (!this._startLonLat) {
            this.renderer?.controls.navigation?.deactivate();
            this._startClick.set(e.x, e.y);
            let coords = e.pickingObject.getCartesian();
            this._startPos = this._planet!.getPixelFromCartesian(coords);
            this._pickedCorner = e.pickingObject;
            if (e.pickingObject.properties.name == "start") {
                this._anchorLonLat = this._cornerEntity[1].getLonLat().clone();
            } else {
                this._anchorLonLat = this._cornerEntity[0].getLonLat().clone();
            }
        }
    };

    protected _onLUp = () => {
        if (this._pickedCorner) {
            this.renderer!.controls.navigation?.activate();
            this._pickedCorner = null;
            this._anchorLonLat = null;
        }
    };

    protected _onCornerLup = () => {
        this._onLUp();
    };

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

    protected _onCornerLeave = (e: IMouseState) => {
        e.renderer.handler.canvas!.style.cursor = "default";
    };

    protected _onLdblclick = () => {
        this._preventClick = true;
    };

    public setVisibility(visibility: boolean) {
        this._cornersLayer.setVisibility(visibility);
        this._trackLayer.setVisibility(visibility);
        this._labelLayer.setVisibility(visibility);
    }

    protected _onLclick = (e: IMouseState) => {
        let startLonLat = this._planet!.getLonLatFromPixelTerrain(e.pos);
        if (!startLonLat) return;
        this._timeout = setTimeout(() => {
            if (!this._preventClick) {
                if (!this._startLonLat) {
                    this.setVisibility(true);
                    this._stopDrawing = false;
                    this._startLonLat = startLonLat!;
                } else {
                    this._startLonLat = null;
                }
            }
            this._preventClick = false;
            this._stopDrawing = false;
            clearTimeout(this._timeout);
        }, 200);

        if (this._startLonLat) {
            this._stopDrawing = true;
        }
    };

    protected _drawLine(startLonLat: LonLat, endLonLat: LonLat, startPos?: Vec3) {
        if (!startPos) {
            startPos = this._planet!.ellipsoid.lonLatToCartesian(startLonLat);
        }

        let endPos = this._planet!.ellipsoid.lonLatToCartesian(endLonLat);

        let res = this._planet!.ellipsoid.inverse(startLonLat, endLonLat);
        let length = res.distance;
        this._heading = res.initialAzimuth;

        let path = [];
        let dir = endPos.sub(startPos);
        let dist = dir.length();
        dir.normalize();

        for (let i = 0; i < OUTLINE_COUNT; i++) {
            let f = dir.scaleTo((i * dist) / OUTLINE_COUNT).addA(startPos);
            path.push(f);
        }
        path.push(endPos);

        this._trackEntity.polyline!.setPath3v([path]);

        if (this._ignoreTerrain) {
            this._propsLabel.setCartesian3v(path[Math.floor(path.length / 2)]);
            this._propsLabel.label!.setText(`${distanceFormat(length)}, ${Math.round(this._heading)} deg`);
        }
    }

    protected _onMouseMove = (e: IMouseState) => {
        if (this._startLonLat && !this._stopDrawing) {
            this._propsLabel.label!.setVisibility(true);
            let endLonLat = this._planet!.getLonLatFromPixelTerrain(e.pos);
            if (!endLonLat) return;
            this._drawLine(this._startLonLat, endLonLat);
        } else if (this._pickedCorner) {
            let newLonLat = this._planet!.getLonLatFromPixelTerrain(e.pos);
            if (newLonLat) {
                if (this._pickedCorner.properties.name === "start") {
                    this._drawLine(newLonLat, this._anchorLonLat!);
                } else {
                    this._drawLine(this._anchorLonLat!, newLonLat);
                }
            }
        }
    };

    public clear() {
        this._trackEntity.remove();
        this._cornerEntity[0].remove();
        this._cornerEntity[1].remove();
        this._propsLabel.remove();
        this._planet!.removeLayer(this._trackLayer);
        this._planet!.removeLayer(this._cornersLayer);
    }

    public isCornersPositionChanged(): boolean {
        let t = this._trackEntity.polyline!.getPath3v()[0] as Vec3[];
        if (t) {
            const startPos = t[0].clone(),
                endPos = t[t.length - 1].clone();
            return (
                this._cornerEntity[0].getCartesian().equal(startPos) &&
                this._cornerEntity[1].getCartesian().equal(endPos)
            );
        }
        return false;
    }

    protected _onForwardpass() {
        let t = this._trackEntity.polyline!.getPath3v()[0] as Vec3[];
        if (t) {
            const startPos = t[0].clone(),
                endPos = t[t.length - 1].clone();

            if (!this.isCornersPositionChanged()) {
                this._cornerEntity[0].setCartesian3v(startPos);
                this._cornerEntity[1].setCartesian3v(endPos);
                if (!this._ignoreTerrain) {
                    let res = 0;
                    for (let i = 0, len = t.length - 1; i < len; i++) {
                        res += t[i + 1].distance(t[i]);
                    }
                    this._propsLabel.setCartesian3v(t[Math.floor(t.length / 2)]);
                    this._propsLabel.label!.setText(`${distanceFormat(res)}, ${Math.round(this._heading)} deg`);
                }
            }
        }
    }

    public get ellipsoid(): Ellipsoid | null {
        return this._planet ? this._planet.ellipsoid : null;
    }
}

const RULERSCENE_EVENTS: RulerSceneEventsList = [
    "add",
    "remove",
    "mousemove",
    "mouseenter",
    "mouseleave",
    "lclick",
    "rclick",
    "mclick",
    "ldblclick",
    "rdblclick",
    "mdblclick",
    "lup",
    "rup",
    "mup",
    "ldown",
    "rdown",
    "mdown",
    "lhold",
    "rhold",
    "mhold",
    "mousewheel",
    "touchmove",
    "touchstart",
    "touchend",
    "doubletouch",
    "touchleave",
    "touchenter"
];

export { RulerScene };