classes/polygon.js

/**
 * Created by Alex Bol on 3/15/2017.
 */

"use strict";

module.exports = function(Flatten) {
    let {Edge, Face, PlanarSet, Box} = Flatten;
    let {ray_shoot} = Flatten;
    /**
     * Class representing a polygon.<br/>
     * Polygon in FlattenJS is a multipolygon comprised from a set of [faces]{@link Flatten.Face}. <br/>
     * Face, in turn, is a closed loop of [edges]{@link Flatten.Edge}, where edge may be segment or circular arc<br/>
     * @type {Polygon}
     */
    Flatten.Polygon = class Polygon {
        /**
         * Constructor creates new instance of polygon.<br/>
         * New polygon is empty. Add face to the polygon using method <br/>
         * <code>
         *     polygon.addFace(Points[]|Segments[]|Arcs[])
         * </code>
         */
        constructor() {
            /**
             * Container of faces (closed loops), may be empty
             * @type {PlanarSet}
             */
            this.faces = new PlanarSet();
            /**
             * Container of edges
             * @type {PlanarSet}
             */
            this.edges = new PlanarSet();
        }

        /**
         * (Getter) Returns bounding box of the polygon
         * @returns {Box}
         */
        get box() {
            return [...this.faces].reduce( (acc, face) => acc.merge(face.box), new Box() );
        }

        /**
         * (Getter) Returns array of vertices
         * @returns {Array}
         */
        get vertices() {
            return [...this.edges].map( edge => edge.start);
        }

        /**
         * Return true is polygon has no edges
         * @returns {boolean}
         */
        isEmpty() {
            return this.edges.size === 0;
        }

        /**
         * Add new face to polygon. Returns added face
         * @param {Points[]|Segments[]|Arcs[]|Circle|Box} args -  new face may be create with one of the following ways: <br/>
         * 1) array of points that describe closed path (edges are segments) <br/>
         * 2) array of shapes (segments and arcs) which describe closed path <br/>
         * 3) circle - will be added as counterclockwise arc <br/>
         * 4) box - will be added as counterclockwise rectangle <br/>
         * You can chain method face.reverse() is you need to change direction of the creates face
         * @returns {Face}
         */
        addFace(...args) {
            let face = new Face(this, ...args);
            this.faces.add(face);
            return face;
        }

        /**
         * Delete existing face from polygon
         * @param {Face} face Face to be deleted
         * @returns {boolean}
         */
        deleteFace(face) {
            for (let edge of face) {
                let deleted = this.edges.delete(edge);
            }
            let deleted = this.faces.delete(face);
            return deleted;
        }

        /**
         * Delete chain of edges from the face.
         * @param {Face} face Face to remove chain
         * @param {Edge} edgeFrom Start of the chain of edges to be removed
         * @param {Edge} edgeTo End of the chain of edges to be removed
         */
        removeChain(face, edgeFrom, edgeTo) {
            // Special case: all edges removed
            if (edgeTo.next === edgeFrom) {
                this.deleteFace(face);
                return;
            }
            for (let edge = edgeFrom; edge !== edgeTo.next; edge = edge.next ) {
                face.remove(this.edges, edge);
                // this.edges.delete(edge);      // delete from PlanarSet of edges and update index
                if (face.isEmpty()) {
                    this.deleteFace(face);    // delete from PlanarSet of faces and update index
                    break;
                }
            }
        }

        /**
         * Add point as a new vertex and split edge. Point supposed to belong to an edge.
         * When edge is split, new edge created from the start of the edge to the new vertex
         * and inserted before current edge.
         * Current edge is trimmed and updated. Method returns new edge added.
         * @param {Edge} edge Edge to be split with new vertex and then trimmed from start
         * @param {Point} pt Point to be added as a new vertex
         * @returns {Edge}
         */
        addVertex(pt, edge) {
            let shapes = edge.shape.split(pt);
            if (shapes.length < 2) return;
            let newEdge = new Flatten.Edge(shapes[0]);
            let edgeBefore = edge.prev;

            /* Insert first split edge into linked list after edgeBefore */
            edge.face.insert(this.edges, newEdge, edgeBefore);

            // Remove old edge from edges container and 2d index
            this.edges.delete(edge);

            // Update edge shape with second split edge keeping links
            edge.shape = shapes[1];

            // Add updated edge to the edges container and 2d index
            this.edges.add(edge);

            return newEdge;
        }

        reverse() {
            for (let face of this.faces) {
                face.reverse();
            }
            return this;
        }

        /**
         * Create new copied instance of the polygon
         * @returns {Polygon}
         */
        clone() {
            let polygon = new Polygon();
            for (let face of this.faces) {
                let shapes = [];
                for (let edge of face) {
                    shapes.push(edge.shape.clone());
                }
                polygon.addFace(shapes);
            }
            return polygon;
        }

        /**
         * Returns area of the polygon. Area of an island will be added, area of a hole will be subtracted
         * @returns {number}
         */
        area() {
            let signedArea = [...this.faces].reduce((acc,face) => acc + face.signedArea(), 0);
            return Math.abs(signedArea);
        }

        /**
         * Returns true if polygon contains point, including polygon boundary, false otherwise
         * Point in polygon test based on ray shooting algorithm
         * @param {Point} point - test point
         * @returns {boolean}
         */
        contains(point) {
            let rel = ray_shoot(this, point);
            return (rel == Flatten.INSIDE || rel == Flatten.BOUNDARY) ? true : false;
        }

        /**
         * Return distance and shortest segment between polygon and other shape as array [distance, shortest_segment]
         * @param {Shape} shape Shape of one of the types Point, Circle, Line, Segment, Arc or Polygon
         * @returns {Number | Segment}
         */
        distanceTo(shape) {
            let {Distance} = Flatten;

            if (shape instanceof Flatten.Point) {
                let [dist, shortest_segment] = Distance.point2polygon(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [dist, shortest_segment];
            }

            if (shape instanceof Flatten.Circle ||
            shape instanceof Flatten.Line ||
            shape instanceof Flatten.Segment ||
            shape instanceof Flatten.Arc) {
                let [dist, shortest_segment] = Distance.shape2polygon(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [dist, shortest_segment];
            }

            /* this method is bit faster */
            if (shape instanceof  Flatten.Polygon) {
                let min_dist_and_segment = [Number.POSITIVE_INFINITY, new Flatten.Segment()];
                let dist, shortest_segment;

                for (let edge of this.edges) {
                    // let [dist, shortest_segment] = Distance.shape2polygon(edge.shape, shape);
                    let min_stop = min_dist_and_segment[0];
                    [dist, shortest_segment] = Distance.shape2planarSet(edge.shape, shape.edges, min_stop);
                    if (Flatten.Utils.LT(dist, min_stop)) {
                        min_dist_and_segment = [dist, shortest_segment];
                    }
                }
                return min_dist_and_segment;
            }
        }

        /**
         * Return array of intersection points between polygon and other shape
         * @param shape Shape of the one of supported types <br/>
         * @returns {Point[]}
         */
        intersect(shape) {
            if (shape instanceof Flatten.Point) {
                return this.contains(shape) ? [shape] : [];
            }

            if (shape instanceof Flatten.Line) {
                return Polygon.intersectLine2Polygon(shape, this);
            }

            if (shape instanceof Flatten.Circle ||
                shape instanceof Flatten.Segment ||
                shape instanceof Flatten.Arc) {
                return Polygon.intersectShape2Polygon(shape, this);
            }

            if (shape instanceof Flatten.Polygon) {
                return Polygon.intersectPolygon2Polygon(shape, this);
            }
        }

        /**
         * Return true if polygon is valid for boolean operations
         * Polygon is valid if <br/>
         * 1. All faces are simple polygons (there are no self-intersected polygons) <br/>
         * 2. All faces are orientable and there is no island inside island or hole inside hole - TODO <br/>
         * 3. There is no intersections between faces (excluding touching) - TODO <br/>
         * @returns {boolean}
         */
        isValid() {
            let valid = true;
            // 1. Polygon is invalid if at least one face is not simple
            for (let face of this.faces) {
                if (!face.isSimple(this.edges)) {
                    valid = false;
                    break;
                }
            }
            // 2. TODO: check if no island inside island and no hole inside hole
            // 3. TODO: check the there is no intersection between faces
            return valid;
        }

        /**
         * Returns new polygon translated by vector vec
         * @param {Vector} vec
         * @returns {Polygon}
         */
        translate(vec) {
            let newPolygon = new Polygon();
            for (let face of this.faces) {
                let shapes = [];
                for (let edge of face) {
                    shapes.push(edge.shape.translate(vec));
                }
                newPolygon.addFace(shapes);
            }
            return newPolygon;
        }

        /**
         * Return new polygon rotated by given angle around given point
         * If point omitted, rotate around origin (0,0)
         * Positive value of angle defines rotation counter clockwise, negative - clockwise
         * @param {number} angle - rotation angle in radians
         * @param {Point} center - rotation center, default is (0,0)
         * @returns {Polygon} - new rotated polygon
         */
        rotate(angle=0, center=new Flatten.Point()) {
            let newPolygon = new Polygon();
            for (let face of this.faces) {
                let shapes = [];
                for (let edge of face) {
                    shapes.push(edge.shape.rotate(angle, center));
                }
                newPolygon.addFace(shapes);
            }
            return newPolygon;
        }

        /**
         * Return new polygon transformed using affine transformation matrix
         * @param {Matrix} matrix - affine transformation matrix
         * @returns {Polygon} - new polygon
         */
        transform(matrix = new Flatten.Matrix()) {
            let newPolygon = new Polygon();
            for (let face of this.faces) {
                let shapes = [];
                for (let edge of face) {
                    shapes.push(edge.shape.transform(matrix));
                }
                newPolygon.addFace(shapes);
            }
            return newPolygon;
        }

        static intersectShape2Polygon(shape, polygon) {
            let ip = [];

            if ( polygon.isEmpty() || shape.box.not_intersect(polygon.box) ) {
                return ip;
            }

            let resp_edges = polygon.edges.search(shape.box);

            for (let edge of resp_edges) {
                for (let pt of shape.intersect(edge.shape)) {
                    ip.push(pt);
                }
            }

            return ip;
        }

        static intersectLine2Polygon(line, polygon) {
            let ip = [];

            if ( polygon.isEmpty() ) {
                return ip;
            }

            for (let edge of polygon.edges) {
                for (let pt of line.intersect(edge.shape)) {
                    ip.push(pt);
                }
            }

            return ip;
        }

        static intersectPolygon2Polygon(polygon1, polygon2) {
            let ip = [];

            if (polygon1.isEmpty() || polygon2.isEmpty()) {
                return ip;
            }

            if (polygon1.box.not_intersect(polygon2.box)) {
                return ip;
            }

            for (let edge1 of polygon1.edges) {
                for (let pt of Polygon.intersectShape2Polygon(edge1.shape, polygon2)) {
                    ip.push(pt);
                }
            }

            return ip;
        }

        /**
         * Return string to draw polygon in svg
         * @param attrs  - an object with attributes for svg path element,
         * like "stroke", "strokeWidth", "fill", "fillRule", "fillOpacity"
         * Defaults are stroke:"black", strokeWidth:"1", fill:"lightcyan", fillRule:"evenodd", fillOpacity: "1"
         * @returns {string}
         */
        svg(attrs = {}) {
            let {stroke, strokeWidth, fill, fillRule, fillOpacity, id, className} = attrs;
            // let restStr = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, "");
            let id_str = (id && id.length > 0) ? `id="${id}"` : "";
            let class_str = (className && className.length > 0) ? `class="${className}"` : "";

            let svgStr = `\n<path stroke="${stroke || "black"}" stroke-width="${strokeWidth || 1}" fill="${fill || "lightcyan"}" fill-rule="${fillRule || "evenodd"}" fill-opacity="${fillOpacity || 1.0}" ${id_str} ${class_str} d="`;
            for (let face of this.faces) {
                svgStr += face.svg();
            }
            svgStr += `" >\n</path>`;
            return svgStr;
        }

        /**
         * This method returns an object that defines how data will be
         * serialized when called JSON.stringify() method
         * @returns {Object}
         */
        toJSON() {
            return [...this.faces].map(face => face.toJSON());
        }
    }
};