1. 程式人生 > >Graham's Scan演算法尋找離散點的凸多邊形(JavaScript版)

Graham's Scan演算法尋找離散點的凸多邊形(JavaScript版)

100個離散點的凸多邊形效果:

 JavaScript程式碼實現如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<canvas id="canvas" style="border:1px solid #aaa;display:block;margin:1px auto;"></canvas>
<div>
    <fieldset>
        <legend>多邊形</legend>
        離散點的個數<input type="text" id="num" value="10">
        <input type="button" value="生成離散點集凸包" onclick="drawPolygon();"/>
    </fieldset>
</div>
<script type="text/javascript">
    //畫布定義(全域性變數)
    var canvas = document.getElementById("canvas");
    canvas.width = 1024;
    canvas.height = 500;
    var context = canvas.getContext("2d");

    //繪製多邊形
    function drawPolygon() {
        //重新生成n個隨機點時清空畫布
        context.clearRect(0, 0, canvas.width, canvas.height);
        //離散點的個數
        var n = document.getElementById("num").value;
        //離散點集
        var discretePointArray = getDiscretePointSet(n);
        //繪製離散點集
        discretePointArray.draw();
        //獲取基點
        var basePoint = getBasePoint(discretePointArray);

        //餘弦值從大到小排序,基點會直接排在第一個
        discretePointArray = quickSort(basePoint, discretePointArray, 0, discretePointArray.length - 1);


        //獲取離散點集凸多邊形頂點
        var polygonVertexSet = getPolygonVertexSet(discretePointArray);
//        for (var i = 0; i < polygonVertexSet.length; i++) {
//            alert(polygonVertexSet[i].x + "," + polygonVertexSet[i].y);
//        }
        //繪製多邊形
        for (var i = 0; i < polygonVertexSet.length; i++) {
            if (i == polygonVertexSet.length - 1) {
                new Vector(polygonVertexSet[i], polygonVertexSet[0]).draw();
            }else {
                new Vector(polygonVertexSet[i], polygonVertexSet[i+1]).draw();
            }

        }
    }

    /**
     * 獲取離散點集凸包
     * @param cosArr 排序後的離散點集
     * @returns {Array}
     */
    function getPolygonVertexSet(cosArr) {
        //凸包點集陣列
        var polygonArr = [];
        //開始獲取(按逆時針掃描,如果排序時升序則需要順時針掃描)
        if (cosArr != null && cosArr.length > 0) {
            polygonArr.push(cosArr[0]); //基點肯定是多邊形頂點
            if (cosArr.length > 1) {
                polygonArr.push(cosArr[1]); //第一個夾角最小的點肯定是多邊形頂點
            }
            if(cosArr.length > 2){
                polygonArr.push(cosArr[2]); //無論是否是多邊形頂點直接放入(回溯中可能會被刪除)
            }
            for (var i = 3; i < cosArr.length; i++) {
                var len = polygonArr.length;
                var leftVector = new Vector(polygonArr[len - 2], polygonArr[len - 1]);
                var rightVector = new Vector(polygonArr[len - 1], cosArr[i]);
                while (leftVector.cross(rightVector) < 0) {//向量叉積小於0時回溯
                    polygonArr.splice(len - 1, 1);//刪除最後一個元素
                    len = polygonArr.length;    //刪除後,len有變化,需要重新設定
                    leftVector = new Vector(polygonArr[len - 2], polygonArr[len - 1]);
                    rightVector = new Vector(polygonArr[len - 1], cosArr[i]);
                }
                polygonArr.push(cosArr[i]);
            }
        }

        return polygonArr;
    }


    /**
     * 平面點物件
     * @param x 點橫座標
     * @param y 點縱座標
     * @param r 繪圖時點的半徑大小
     * @constructor
     */
    function Point(x, y, r) {
        this.x = x;
        this.y = y;
        this.r = r;
        //繪製點
        this.draw = function () {
            var startAngle = 0 * Math.PI; //實心點的開始弧度
            var endAngle = 2 * Math.PI; //實心點的結束弧度
            //開始繪製實心點
            context.beginPath();
            context.arc(this.x, this.y, this.r, startAngle, endAngle);
            context.fillStyle = "rgba(255,0,255,.8)";
            context.fill();
            context.closePath();
        }
    }

    /**
     * 平面向量物件
     * @param start 向量始點
     * @param end 向量終點
     * @constructor
     */
    function Vector(start, end) {
        this.start = start;
        this.end = end;
        // 向量座標
        this.x = this.end.x - this.start.x;
        this.y = this.end.y - this.start.y;
        // 向量與x軸正向的夾角餘弦值
        // 零向量(0,0)與x軸正向的夾角餘弦值定義為2,按餘弦值降序排序時排在第一個位置
        this.cosx = (this.x == 0 && this.y == 0) ? 2
            : (this.x / Math.sqrt(this.x * this.x + this.y * this.y));
        // 向量叉積
        this.cross = function (that) {
            var result = this.x * that.y - that.x * this.y;

            return result;
        }
        //繪製向量
        this.draw = function () {
            context.moveTo(this.start.x, this.start.y);       //設定起點狀態
            context.lineTo(this.end.x, this.end.y);       //設定末端狀態
            context.lineWidth = 1;          //設定線寬狀態
            context.strokeStyle = "red";  //設定線的顏色狀態
            context.stroke();               //進行繪製
        }
    }

    /**
     * 獲取基點:在離散點集中選取y座標最小的點,當作開始點
     * 如果存在多個點的y座標都為最小值,則選取x座標最小的一點
     * @param vertexSet 離散點集
     * @returns {*}
     */
    function getBasePoint(vertexSet) {
        if (vertexSet != null && vertexSet.length > 0) {
            var point = vertexSet[0];
            for (var i = 1; i < vertexSet.length; i++) {
                //最小y(多個y相同時,選擇x最小的點)
                if (vertexSet[i].y < point.y ||
                    ((vertexSet[i].y == point.y) && (vertexSet[i].x < point.x))) {
                    point = vertexSet[i];
                }
            }

            return point;
        }

        return null;
    }

    /**
     * 隨機生成一個離散點
     * @returns {Point} 生成的離散點
     */
    function createRandomPoint() {
        var r = 3;
        //橫座標
        var x = Math.random() * (canvas.width - 2 * r) + r;
        //縱座標
        var y = Math.random() * (canvas.height - 2 * r) + r;
//        x = Math.floor(x);
//        y = Math.floor(y);

        return new Point(x, y, r);
    }


    /**
     * 隨機生成n個不重合的離散點
     * @param n 離散點的個數
     * @returns {Array} 離散點集
     */
    function getDiscretePointSet(n) {
        var vertexArray = [];
        if (n != null && n > 0) {
            for (var i = 0; i < n; i++) {
                var point = createRandomPoint(); //離散點
                //離散點不能有重合,重合點需要重新生成
                for (var j = 0; j < vertexArray.length; j++) {
                    //注:此處理論上會出現死迴圈
                    while ((point.x == vertexArray[j].x) && (point.y == vertexArray[j].y)) {
                        point = createRandomPoint();
                        j = 0; //重新生成點後從頭開始新一輪比較
                    }
                }
                vertexArray.push(point);
            }
        }

        return vertexArray;
    }

    /**
     * 對離散點排序:按照其與基點構成的向量與x軸正方向夾角餘弦值快速降序
     * @param basePoint 基點
     * @param discretePointArray 需要排序的離散點集
     * @param left 左指示變數
     * @param right 右指示變數
     * @returns {*}
     */
    function quickSort(basePoint, discretePointArray, left, right) {
        var i = left;
        var j = right;
        var temp = discretePointArray[left];
        var tempV = new Vector(basePoint, temp);
        while (i < j) {
            while (i < j && tempV.cosx > new Vector(basePoint, discretePointArray[j]).cosx) {
                j--;
            }
            if (i < j) {
                discretePointArray[i++] = discretePointArray[j];
            }
            while (i < j && tempV.cosx < new Vector(basePoint, discretePointArray[i]).cosx) {
                i++;
            }
            if (i < j) {
                discretePointArray[j--] = discretePointArray[i];
            }
        }
        discretePointArray[i] = temp;
        if (left < i) {
            quickSort(basePoint, discretePointArray, left, i - 1);
        }
        if (right > i) {
            quickSort(basePoint, discretePointArray, i + 1, right);
        }

        return discretePointArray;
    }

    /**
     * 擴充套件Array方法:繪製整個點集
     */
    Array.prototype.draw = function () {
        if (this.length > 0) {
            for (var i = 0; i < this.length; i++) {
                if (this[i] instanceof Point) {
                    this[i].draw();
                }
            }
        }
    }
</script>
</body>
</html>