Graham's Scan演算法尋找離散點的凸多邊形(JavaScript版)
阿新 • • 發佈:2018-12-14
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>