entity/geometry/Geometry.tstypescript
import * as utils from "../../utils/shared";
import { Entity } from "../Entity";
import { Extent } from "../../Extent";
import { GeometryHandler } from "./GeometryHandler";
import { LonLat } from "../../LonLat";
import { Vec4 } from "../../math/Vec4";
import type { NumberArray4 } from "../../math/Vec4";
import type { NumberArray2 } from "../../math/Vec2";
import type { NumberArray3 } from "../../math/Vec3";

export enum GeometryTypeEnum {
    POINT = 1,
    LINESTRING = 2,
    POLYGON = 3,
    MULTIPOLYGON = 4,
    MULTILINESTRING = 5
}

const GeometryType: Record<string, number> = {
    POINT: 1,
    LINESTRING: 2,
    POLYGON: 3,
    MULTIPOLYGON: 4,
    MULTILINESTRING: 5
};

export type CoordinatesType = NumberArray2 | NumberArray3;
// {
//     "type": "Point",
//     "coordinates": [100.0, 0.0]
// }
export type IPointCoordinates = CoordinatesType;

// {
//     "type": "LineString",
//     "coordinates": [
//         [100.0, 0.0],
//         [101.0, 1.0]
//     ]
// }
export type ILineStringCoordinates = CoordinatesType[];

// No holes
// {
//     "type": "Polygon",
//     "coordinates": [
//         [
//             [100.0, 0.0],
//             [101.0, 0.0],
//             [101.0, 1.0],
//             [100.0, 1.0],
//             [100.0, 0.0]
//         ]
//     ]
// }
//
// With holes:
// {
//     "type": "Polygon",
//     "coordinates": [
//         [
//             [100.0, 0.0],
//             [101.0, 0.0],
//             [101.0, 1.0],
//             [100.0, 1.0],
//             [100.0, 0.0]
//         ],
//         [
//             [100.8, 0.8],
//             [100.8, 0.2],
//             [100.2, 0.2],
//             [100.2, 0.8],
//             [100.8, 0.8]
//         ]
//     ]
// }
export type IPolygonCoordinates = CoordinatesType[][];

// {
//     "type": "MultiLineString",
//     "coordinates": [
//         [
//             [100.0, 0.0],
//             [101.0, 1.0]
//         ],
//         [
//             [102.0, 2.0],
//             [103.0, 3.0]
//         ]
//     ]
// }
export type IMultiLineStringCoordinates = ILineStringCoordinates[];

// {
//     "type": "MultiPolygon",
//     "coordinates": [
//         [
//             [
//                 [102.0, 2.0],
//                 [103.0, 2.0],
//                 [103.0, 3.0],
//                 [102.0, 3.0],
//                 [102.0, 2.0]
//             ]
//         ],
//         [
//             [
//                 [100.0, 0.0],
//                 [101.0, 0.0],
//                 [101.0, 1.0],
//                 [100.0, 1.0],
//                 [100.0, 0.0]
//             ],
//             [
//                 [100.2, 0.2],
//                 [100.2, 0.8],
//                 [100.8, 0.8],
//                 [100.8, 0.2],
//                 [100.2, 0.2]
//             ]
//         ]
//     ]
// }
export type IMultiPolygonCoordinates = IPolygonCoordinates[];

export type IGeometryCoordinates =
    | IPointCoordinates
    | IPolygonCoordinates
    | IMultiPolygonCoordinates
    | ILineStringCoordinates
    | IMultiLineStringCoordinates;

export type GeometryTypeToCoordinates = {
    POINT: IPointCoordinates;
    LINESTRING: ILineStringCoordinates;
    POLYGON: IPolygonCoordinates;
    MULTIPOLYGON: IMultiPolygonCoordinates;
    MULTILINESTRING: IMultiLineStringCoordinates;
};

interface IGeometry<T extends keyof typeof GeometryTypeEnum = keyof typeof GeometryTypeEnum> {
    type: T;
    coordinates: GeometryTypeToCoordinates[T];
}

export interface IGeometryStyle {
    fillColor?: string | NumberArray4 | Vec4;
    lineColor?: string | NumberArray4 | Vec4;
    strokeColor?: string | NumberArray4 | Vec4;
    lineWidth?: number;
    strokeWidth?: number;
}

interface IGeometryStyleInternal {
    fillColor: Vec4;
    lineColor: Vec4;
    strokeColor: Vec4;
    lineWidth: number;
    strokeWidth: number;
}

export interface IGeometryParams<T extends keyof typeof GeometryTypeEnum = keyof typeof GeometryTypeEnum> {
    type?: T;
    coordinates?: GeometryTypeToCoordinates[T];
    style?: IGeometryStyle;
    visibility?: boolean;
}

class Geometry {
    static __counter__: number = 0;

    public __id: number;

    /**
     * Entity instance that holds this geometry.
     * @public
     * @type {Entity}
     */
    public _entity: Entity | null;

    public _handler: GeometryHandler | null;
    public _handlerIndex: number;

    // Polygon
    public _polyVerticesHighMerc: number[];
    public _polyVerticesLowMerc: number[];
    public _polyVerticesLength: number;
    public _polyIndexesLength: number;
    public _polyVerticesHandlerIndex: number;
    public _polyIndexesHandlerIndex: number;

    // Line(Linestring and polygon's stroke(s)
    public _lineVerticesHighMerc: number[];
    public _lineVerticesLowMerc: number[];
    public _lineVerticesLength: number;
    public _lineOrdersLength: number;
    public _lineIndexesLength: number;
    public _lineColorsLength: number;
    public _lineThicknessLength: number;
    public _lineVerticesHandlerIndex: number;
    public _lineOrdersHandlerIndex: number;
    public _lineIndexesHandlerIndex: number;
    public _lineThicknessHandlerIndex: number;
    public _lineColorsHandlerIndex: number;
    protected _type: GeometryTypeEnum;
    public _coordinates: IGeometryCoordinates;
    protected _extent: Extent;
    public _style: IGeometryStyleInternal;
    protected _visibility: boolean;
    public _pickingReady: boolean;

    constructor(options: IGeometryParams = {}) {
        this.__id = Geometry.__counter__++;

        this._entity = null;

        this._handler = null;
        this._handlerIndex = -1;

        this._polyVerticesHighMerc = [];
        this._polyVerticesLowMerc = [];
        this._polyVerticesLength = -1;
        this._polyIndexesLength = -1;
        this._polyVerticesHandlerIndex = -1;
        this._polyIndexesHandlerIndex = -1;

        this._lineVerticesHighMerc = [];
        this._lineVerticesLowMerc = [];
        this._lineVerticesLength = -1;
        this._lineOrdersLength = -1;
        this._lineIndexesLength = -1;
        this._lineColorsLength = -1;
        this._lineThicknessLength = -1;
        this._lineVerticesHandlerIndex = -1;
        this._lineOrdersHandlerIndex = -1;
        this._lineIndexesHandlerIndex = -1;
        this._lineThicknessHandlerIndex = -1;
        this._lineColorsHandlerIndex = -1;

        this._type = (options.type && Geometry.getType(options.type)) || GeometryTypeEnum.POINT;
        this._coordinates = options.coordinates || [];
        this._extent = Geometry.getExtent(
            {
                type: options.type || "POINT",
                coordinates: options.coordinates || []
            },
            this._coordinates
        );

        options.style = options.style || {};

        this._style = {
            fillColor: utils.createColorRGBA(options.style.fillColor, new Vec4(0.19, 0.62, 0.85, 0.4)),
            lineColor: utils.createColorRGBA(options.style.lineColor, new Vec4(0.19, 0.62, 0.85, 1)),
            strokeColor: utils.createColorRGBA(options.style.strokeColor, new Vec4(1, 1, 1, 0.95)),
            lineWidth: options.style.lineWidth || 3,
            strokeWidth: options.style.strokeWidth || 0
        };

        this._visibility = options.visibility || true;

        // optimization flag for picking mask rendering pass
        this._pickingReady = false;
    }

    public get id(): number {
        return this.__id;
    }

    public get type(): number {
        return this._type;
    }

    static getType(typeStr: keyof typeof GeometryTypeEnum): GeometryTypeEnum {
        return GeometryType[typeStr.toUpperCase()];
    }

    /**
     * Returns geometry extent.
     * @static
     * @param {IGeometry} geometryObj - GeoJSON-style geometry feature.
     * @param {IGeometryCoordinates} outCoordinates - Cloned geometry coordinates.
     * @returns {Extent} Geometry extent.
     */
    static getExtent(geometryObj: IGeometry, outCoordinates: IGeometryCoordinates): Extent {
        let res = new Extent(new LonLat(180.0, 90.0), new LonLat(-180.0, -90.0));
        let t = Geometry.getType(geometryObj.type);

        if (t === GeometryTypeEnum.POINT) {
            let lon: number = geometryObj.coordinates[0] as number,
                lat: number = geometryObj.coordinates[1] as number;
            res.southWest.lon = lon;
            res.southWest.lat = lat;
            res.northEast.lon = lon;
            res.northEast.lat = lat;
            outCoordinates && (outCoordinates[0] = lon) && (outCoordinates[1] = lat);
        } else if (t === GeometryTypeEnum.LINESTRING) {
            let c: ILineStringCoordinates = geometryObj.coordinates as ILineStringCoordinates;
            for (let i = 0; i < c.length; i++) {
                let lon = c[i][0],
                    lat = c[i][1];
                if (lon < res.southWest.lon) res.southWest.lon = lon;
                if (lat < res.southWest.lat) res.southWest.lat = lat;
                if (lon > res.northEast.lon) res.northEast.lon = lon;
                if (lat > res.northEast.lat) res.northEast.lat = lat;
                outCoordinates && ((outCoordinates as ILineStringCoordinates)[i] = [lon, lat]);
            }
        } else if (t === GeometryTypeEnum.POLYGON) {
            let c: IPolygonCoordinates = geometryObj.coordinates as IPolygonCoordinates;
            for (let i = 0; i < c.length; i++) {
                let ci: CoordinatesType[] = c[i];
                outCoordinates && (outCoordinates[i] = []);
                for (let j = 0; j < ci.length; j++) {
                    let cij = ci[j];
                    let lon = cij[0],
                        lat = cij[1];
                    if (lon < res.southWest.lon) res.southWest.lon = lon;
                    if (lat < res.southWest.lat) res.southWest.lat = lat;
                    if (lon > res.northEast.lon) res.northEast.lon = lon;
                    if (lat > res.northEast.lat) res.northEast.lat = lat;
                    outCoordinates && ((outCoordinates as IPolygonCoordinates)[i][j] = [lon, lat]);
                }
            }
        } else if (t === GeometryTypeEnum.MULTIPOLYGON) {
            let p = geometryObj.coordinates;
            for (let i = 0; i < p.length; i++) {
                let pi: IPolygonCoordinates = p[i] as IPolygonCoordinates;
                outCoordinates && (outCoordinates[i] = []);
                for (let j = 0; j < pi.length; j++) {
                    let pij: CoordinatesType[] = pi[j];
                    outCoordinates && ((outCoordinates as IMultiPolygonCoordinates)[i][j] = []);
                    for (let k = 0; k < pij.length; k++) {
                        let pijk = pij[k];
                        let lon = pijk[0],
                            lat = pijk[1];
                        if (lon < res.southWest.lon) res.southWest.lon = lon;
                        if (lat < res.southWest.lat) res.southWest.lat = lat;
                        if (lon > res.northEast.lon) res.northEast.lon = lon;
                        if (lat > res.northEast.lat) res.northEast.lat = lat;
                        outCoordinates && ((outCoordinates as IMultiPolygonCoordinates)[i][j][k] = [lon, lat]);
                    }
                }
            }
        } else if (t === GeometryTypeEnum.MULTILINESTRING) {
            let c = geometryObj.coordinates;
            for (let i = 0; i < c.length; i++) {
                let ci: ILineStringCoordinates = c[i] as ILineStringCoordinates;
                outCoordinates && (outCoordinates[i] = []);
                for (let j = 0; j < ci.length; j++) {
                    let cij = ci[j];
                    let lon = cij[0],
                        lat = cij[1];
                    if (lon < res.southWest.lon) res.southWest.lon = lon;
                    if (lat < res.southWest.lat) res.southWest.lat = lat;
                    if (lon > res.northEast.lon) res.northEast.lon = lon;
                    if (lat > res.northEast.lat) res.northEast.lat = lat;
                    outCoordinates && ((outCoordinates as IMultiLineStringCoordinates)[i][j] = [lon, lat]);
                }
            }
        } else {
            res.southWest.lon = res.southWest.lat = res.northEast.lon = res.northEast.lat = 0.0;
            outCoordinates && (outCoordinates[0] = 0) && (outCoordinates[1] = 0);
        }

        return res;
    }

    /**
     * @todo ASAP need test for this method
     * @param geoJson
     * @returns {Geometry}
     */
    public setGeometry(geoJson: IGeometry): Geometry {
        let h = this._handler;
        if (h) {
            this.remove();
            this._type = Geometry.getType(geoJson.type || "Point");
            this._extent = Geometry.getExtent(geoJson, this._coordinates);
            h.add(this);
        }
        return this;
    }

    public setFillColor(r: number, g: number, b: number, a: number = 1.0): Geometry {
        let c = this._style.fillColor;
        if ((c.w === 0.0 && a !== 0.0) || (c.w !== 0.0 && a === 0.0)) {
            this._pickingReady = false;
        }
        c.x = r;
        c.y = g;
        c.z = b;
        c.w = a;
        this._handler && this._handler.setPolyColorArr(this, c);
        return this;
    }

    public overlaps(extent: Extent): boolean {
        return this._extent.overlaps(extent);
    }

    public setFillColor4v(rgba: Vec4): Geometry {
        return this.setFillColor(rgba.x, rgba.y, rgba.z, rgba.w);
    }

    public setStrokeColor(r: number, g: number, b: number, a: number = 1.0): Geometry {
        let c = this._style.strokeColor;
        if ((c.w === 0.0 && a !== 0.0) || (c.w !== 0.0 && a === 0.0)) {
            this._pickingReady = false;
        }
        c.x = r;
        c.y = g;
        c.z = b;
        c.w = a;
        this._handler && this._handler.setLineStrokeColorArr(this, c);
        return this;
    }

    public setLineColor(r: number, g: number, b: number, a: number = 1.0): Geometry {
        let c = this._style.lineColor;
        if ((c.w === 0.0 && a !== 0.0) || (c.w !== 0.0 && a === 0.0)) {
            this._pickingReady = false;
        }
        c.x = r;
        c.y = g;
        c.z = b;
        c.w = a;
        this._handler && this._handler.setLineColorArr(this, c);
        return this;
    }

    public setStrokeColor4v(rgba: Vec4): Geometry {
        return this.setStrokeColor(rgba.x, rgba.y, rgba.z, rgba.w);
    }

    public setLineColor4v(rgba: Vec4): Geometry {
        return this.setLineColor(rgba.x, rgba.y, rgba.z, rgba.w);
    }

    /**
     * Gets geometry fill color.
     * @public
     * @returns {Vec4} Copy of the normalized RGBA fill color.
     */
    public getFillColor4v(): Vec4 {
        return this._style.fillColor.clone();
    }

    /**
     * Gets geometry line color.
     * @public
     * @returns {Vec4} Copy of the normalized RGBA line color.
     */
    public getLineColor4v(): Vec4 {
        return this._style.lineColor.clone();
    }

    /**
     * Gets geometry stroke color.
     * @public
     * @returns {Vec4} Copy of the normalized RGBA stroke color.
     */
    public getStrokeColor4v(): Vec4 {
        return this._style.strokeColor.clone();
    }

    public setStrokeOpacity(opacity: number): Geometry {
        let c = this._style.strokeColor;
        c.w = opacity;
        return this.setStrokeColor(c.x, c.y, c.z, opacity);
    }

    public setLineOpacity(opacity: number): Geometry {
        let c = this._style.lineColor;
        c.w = opacity;
        return this.setLineColor(c.x, c.y, c.z, opacity);
    }

    public setStrokeWidth(width: number): Geometry {
        this._style.strokeWidth = width;
        this._pickingReady = false;
        this._handler && this._handler.setLineStrokeArr(this, width);
        return this;
    }

    public bringToFront(): Geometry {
        this._handler && this._handler.bringToFront(this);
        return this;
    }

    public setLineWidth(width: number): Geometry {
        this._style.lineWidth = width;
        this._pickingReady = false;
        this._handler && this._handler.setLineThicknessArr(this, width);
        return this;
    }

    public setFillOpacity(opacity: number): Geometry {
        let c = this._style.fillColor;
        if ((c.w === 0.0 && opacity !== 0.0) || (c.w !== 0.0 && opacity === 0.0)) {
            this._pickingReady = false;
        }
        c.w = opacity;
        this._handler && this._handler.setPolyColorArr(this, c);
        return this;
    }

    public setVisibility(visibility: boolean): Geometry {
        this._visibility = visibility;
        this._handler && this._handler.setGeometryVisibility(this);
        return this;
    }

    public getVisibility(): boolean {
        return this._visibility;
    }

    public remove() {
        this._handler && this._handler.remove(this);
    }

    public getExtent(): Extent {
        return this._extent.clone();
    }

    public getType(): number {
        return this._type;
    }
}

export { Geometry };