html5 canvas 模擬 迪傑斯特拉算法( Dijkstra )求最短路徑

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
   <style type="text/css">
        .active{
            background-color: green;
            color: black;
        }

        .no_active{
            background-color: orange;
            color: black;
            cursor: pointer;
        }
   </style>
</head>
<body id="body">
    <button id="btn_step1" disabled class="active" title="畫節點模式:點擊紅框內任意位置就會在該位置生成一個節點!">畫圓</button>
    <button id="btn_step2" class="no_active" title="畫路徑模式: 前後點擊任意兩個節點,則會在這兩個節點間生成一個連線,併產生一個隨機的路徑距離!">畫線</button>
    <button id="btn_step3" class="no_active" title="計算最短路徑模式: 前後點擊任意兩個節點,則會計算最短路徑並用紅線標出最短路徑!">求最短距離</button>

    <br/>
    <!-- <canvas id="myCanvas" width="200" height="100" style="border:1px solid red;"></canvas> -->
</body>
<script type="text/javascript">
    // 初始化一些常量
    var canvas_width = 500; // 畫布寬度
    var canvas_height = 300; //畫布高度
    var circleR=20;// 圓的半徑

    // 記錄一些創建的對象信息
    var circleCenterCoordinateArr = []; //記錄創建的圓的圓心座標的數組
    var firstClickCircleIndex = -1;
    var secondClickCircleIndex = -1;

    var infinite = 999999;
    var defaultColor = "black";

    var step1 =true; // 點擊生成節點
    var step2=false; //點擊生成節點連線
    var step3=false; //點擊計算兩點之間最短路徑

    var graph = [];// 圖結構的存儲,是個二位數組,graph[ i ][ j ]=10 表示 第i個節點和第j個節點的距離是10,如果這兩個節點沒有連線,則值爲正無窮

    // 初始化  canvas  畫布元素
    var dom_canvas = document.createElement( "canvas" );
    dom_canvas.setAttribute( "id","myCanvas" );
    dom_canvas.setAttribute( "width",canvas_width );
    dom_canvas.setAttribute( "height",canvas_height );
    dom_canvas.setAttribute( "style","border:1px solid red;" );
    document.getElementById( "body" ).appendChild( dom_canvas );

    // 初始化 畫筆對象 ( 得到畫布的畫筆對象 )
    var pen = dom_canvas.getContext( "2d" );

    // 圓的角標
    var circleIndex = 0;
    // 爲 畫布 添加點擊事件,點擊一下就畫一個圓,圓心就是點擊的位置
    dom_canvas.onclick = function( event ){
        if( step1 ){
            handleDrawCircleEvent( event );
        }else if( step2 ){
            handleDrawLineEvent( event );
        }else if( step3 ){
            handleComputeShortestPathEvent( event );
        }
    }

    function handleDrawCircleEvent( event ){
        var x = event.clientX - dom_canvas.getBoundingClientRect().left;
        var y = event.clientY - dom_canvas.getBoundingClientRect().top;
       // console.log( "( " + x + "," + y + " )" );
        // 畫一個圓形邊框
        drawCircleWithText( x,y,circleR,"V" + circleIndex,20,defaultColor );

        // 將圓心座標存儲起來
        circleCenterCoordinateArr[ circleIndex ] = [ x,y ];

        // 將該圓的編號存儲至圖結構中
        /*
            |   | V0| V1| V2| V3| V4|
            | VO| 0 | ∞ | ∞ | ∞ | ∞ |
            | V1| ∞ | 0 | ∞ | ∞ | ∞ | 
            | V2| ∞ | ∞ | 0 | ∞ | ∞ | 
            | V3| ∞ | ∞ | ∞ | 0 | ∞ | 
            | V4| ∞ | ∞ | ∞ | ∞ | 0 |
        */
        graph[ circleIndex ]=[];
        for( var i=0;i<=circleIndex;i++ ){
            if( i == circleIndex ){
                graph[ circleIndex ][ i ] = 0;
            }else{
                graph[ circleIndex ][ i ] = infinite;
                graph[ i ][ circleIndex ] = infinite;
            }
        }
        circleIndex++;
        printGraph();
    }


    function printGraph(){
        var titleRow = "|   |";
        for( var i=0;i<graph.length;i++ ){
            titleRow+=( " V" + i + "|" );
        }
        console.log( titleRow );
        for( var i=0;i<graph.length;i++ ){
            var row = "| V" + i + "|";
            for( var j=0;j<graph[ i ].length;j++ ){
                if( graph[ i ][ j ] == infinite ){
                    row +=( " ∞ |" );
                }else{
                    row +=( " " + graph[ i ][ j ] + " |" );
                }
            }
            console.log( row );
        }
        console.log( "" );
    }

    function handleDrawLineEvent( event ){
        // 判斷當前點擊的點在哪一個 圓的內部
        var x = event.clientX - dom_canvas.getBoundingClientRect().left;
        var y = event.clientY - dom_canvas.getBoundingClientRect().top;
        var  clickedCircleIndex = -1;
        for( var i=0;i<circleCenterCoordinateArr.length;i++ ){
            var x_circle = circleCenterCoordinateArr[ i ][ 0 ];
            var y_circle = circleCenterCoordinateArr[ i ][ 1 ];
            if( checkInCircle( x,y,x_circle,y_circle,circleR ) ){
                clickedCircleIndex = i;
                break;
            }
        }
        if( clickedCircleIndex == -1 && firstClickCircleIndex == -1 ){
            alert( "請選擇連線開始節點!" );
            return;
        }

        if( clickedCircleIndex == -1 && secondClickCircleIndex == -1 ){
            alert( "請選擇連線結束節點!" );
            return;
        }

        if( firstClickCircleIndex == -1 ){
            firstClickCircleIndex = clickedCircleIndex;
            console.log("點擊的開始節點是  V" + firstClickCircleIndex );
            return;
        }
        if( secondClickCircleIndex == -1 ){
            secondClickCircleIndex = clickedCircleIndex;
            console.log( "點擊的結束節點是  V" + secondClickCircleIndex );
            console.log( "" );

            // 畫2個圓之間的連線
            var randomDistance = Math.ceil( Math.random() * 10 );
            drawConnectionTwoCircle( firstClickCircleIndex,secondClickCircleIndex,defaultColor,randomDistance );

            // 更新圖結構
            graph[ firstClickCircleIndex ][ secondClickCircleIndex ] = randomDistance;
            graph[ secondClickCircleIndex ][ firstClickCircleIndex ] = randomDistance;
            printGraph();

            firstClickCircleIndex = -1;
            secondClickCircleIndex = -1;
            return;
        }
    }

    // 畫2個圓之間的連線
    function drawConnectionTwoCircle( circleIndex1,circleIndex2,color,distance ){
        // circleIndex1 和 circleIndex2 都有了,開始連線
        var x1 = circleCenterCoordinateArr[ circleIndex1 ][ 0 ];
        var y1 = circleCenterCoordinateArr[ circleIndex1 ][ 1 ];
        var x2 = circleCenterCoordinateArr[ circleIndex2 ][ 0 ];
        var y2 = circleCenterCoordinateArr[ circleIndex2 ][ 1 ];

        var l=Math.sqrt( ( x1 - x2 ) * ( x1 - x2 ) + ( y1 - y2 ) * ( y1 - y2 ) );
        var x_from = ( x2 - x1 ) * circleR / l + x1;
        var y_from = ( y2 - y1 ) * circleR / l + y1;

        var x_to = x2 - ( x2 - x1 ) * circleR / l;
        var y_to = y2 - ( y2 - y1 ) * circleR / l;

        drawLine( x_from,y_from,x_to,y_to,color );
        // 爲此連線生成一個隨機的距離,並寫到 此連線中點的位置
        var x_middle = ( x1 + x2 )/2;
        var y_middle = ( y1 + y2 )/2;
        
        if( !distance ){
            distance = Math.ceil( Math.random() * 10 );
        }
        if( color ){
            pen.fillStyle = color;
        }else{
            pen.fillStyle = defaultColor;
        }
        pen.fillText( distance,x_middle,y_middle );
    }
    
    // 畫一個圓形邊框,並在圓心寫文字
    function drawCircleWithText( x,y,r,text,fontSize,color ){
        if( color ){
            pen.strokeStyle = color;
            pen.fillStyle = color;
        }else{
            pen.strokeStyle = defaultColor;
            pen.fillStyle = defaultColor;
        }
        pen.beginPath();
        pen.arc(x,y,r,0,2*Math.PI);
        pen.stroke();
        pen.closePath();
        pen.font = fontSize + "px Microsoft JhengHei";
        pen.fillText( text,x - fontSize/2,y + fontSize/2 );
    }

    // 畫一根線
    function drawLine( x1,y1,x2,y2,color ){
        if( color ){
            pen.strokeStyle = color;
        }else{
            pen.strokeStyle = defaultColor;
        }
        pen.beginPath();
        pen.moveTo( x1,y1 );
        pen.lineTo( x2,y2 );
        pen.stroke();
        pen.closePath();
    }

    document.getElementById( "btn_step1" ).onclick=function(){
        step1=true;
        step2=false;
        step3=false;
        document.getElementById( "btn_step1" ).setAttribute( "disabled","disabled" );
        document.getElementById( "btn_step1" ).setAttribute( "class","active" )

        document.getElementById( "btn_step2" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step2" ).setAttribute( "class","no_active" );

        document.getElementById( "btn_step3" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step3" ).setAttribute( "class","no_active" );
    }

    document.getElementById( "btn_step2" ).onclick=function(){
        step2=true;
        step1=false;
        step3=false;
        document.getElementById( "btn_step2" ).setAttribute( "disabled","disabled" );
        document.getElementById( "btn_step2" ).setAttribute( "class","active" )

        document.getElementById( "btn_step1" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step1" ).setAttribute( "class","no_active" );

        document.getElementById( "btn_step3" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step3" ).setAttribute( "class","no_active" );
    }

    document.getElementById( "btn_step3" ).onclick=function(){
        step3=true;
        step1=false;
        step2=false;
        document.getElementById( "btn_step3" ).setAttribute( "disabled","disabled" );
        document.getElementById( "btn_step3" ).setAttribute( "class","active" )

        document.getElementById( "btn_step1" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step1" ).setAttribute( "class","no_active" );

        document.getElementById( "btn_step2" ).removeAttribute( "disabled" );
        document.getElementById( "btn_step2" ).setAttribute( "class","no_active" );
    }

    // 檢測 點 x,y 是否在 圓內部( 圓心座標爲x_circle,y_circle,圓半徑爲 r  )
    function checkInCircle( x,y,x_circle,y_circle,r ){
        //求點到圓心的距離,用到了勾股定理
        var dis = Math.sqrt( ( x - x_circle ) * ( x - x_circle ) + ( y - y_circle ) * ( y - y_circle ) );//Math.sqrt()求平方跟
        if(dis <= r){
            return true;
        }
        return false;
    }


    var startCircleIndex = -1;
    var endCircleIndex = -1;
    function handleComputeShortestPathEvent( event ){
        // 判斷當前點擊的點在哪一個 圓的內部
        var x = event.clientX - dom_canvas.getBoundingClientRect().left;
        var y = event.clientY - dom_canvas.getBoundingClientRect().top;
        var  clickedCircleIndex = -1;
        for( var i=0;i<circleCenterCoordinateArr.length;i++ ){
            var x_circle = circleCenterCoordinateArr[ i ][ 0 ];
            var y_circle = circleCenterCoordinateArr[ i ][ 1 ];
            if( checkInCircle( x,y,x_circle,y_circle,circleR ) ){
                clickedCircleIndex = i;
                break;
            }
        }
        if( clickedCircleIndex == -1 && startCircleIndex == -1 ){
            alert( "請選擇出發節點!" );
            return;
        }

        if( clickedCircleIndex == -1 && endCircleIndex == -1 ){
            alert( "請選擇到達節點!" );
            return;
        }

        if( startCircleIndex == -1 ){
            startCircleIndex = clickedCircleIndex;
            console.log("選擇的出發節點是  V" + startCircleIndex );
            return;
        }
        if( endCircleIndex == -1 ){
            endCircleIndex = clickedCircleIndex;
            console.log( "選擇的到達節點是  V" + endCircleIndex );
            console.log( "" );

            // startCircleIndex 和 endCircleIndex 都有了,開始計算最短路徑
    /*
                |   | V0| V1| V2| V3| V4| V5|
                | V0| 0 | 8 | 6 | ∞ | ∞ | 10 |
                | V1| 8 | 0 | ∞ | 1 | ∞ | 3  |
                | V2| 6 | ∞ | 0 | ∞ | 3 | 5  |
                | V3| ∞ | 1 | ∞ | 0 | 1 | 10 |
                | V4| ∞ | ∞ | 3 | 1 | 0 | 2  |
                | V5| 10 | 3 | 5 | 10 | 2 | 0 |
*/
            //todo  最難的部分來了
            // 解析 graph  二維數組
            // 例如: 計算 v0到 v4的最短路徑
            // 分別 計算 v0 到 v1 v2 v3 v4 v5的最短路徑
            var distanceObjArr= [];
            var uncheckedMinIndex=-1;
            var minDistance=infinite;
            for( var i = 0; i < graph.length; i++ ){
                if( i == startCircleIndex ){
                    continue;
                }
                var currDistance= graph[ startCircleIndex ][ i ];
                if( currDistance < minDistance ){
                    minDistance = currDistance;
                    uncheckedMinIndex = i;
                }
                distanceObjArr[ i ] = {
                                        "checked":false,
                                        "path": startCircleIndex + "," + i + ",",
                                        "distance":currDistance
                                     }
            }
            printDistanceObjArr( distanceObjArr );

            while( true ){
                // if( distanceObjArr[ endCircleIndex ].checked ){
                if( uncheckedMinIndex== -1 ){
                    break;
                }
                // V0 到  uncheckedMinIndex 節點的最短距離是  distanceObjArr[ uncheckedMinIndex ].distance
                // 所以嘗試計算 V0 途徑 uncheckedMinIndex 節點會不會使得到達其他節點的距離縮短
                minDistance = infinite;
                var nextUncheckedMinIndex = -1;
                for( var i = 0; i < distanceObjArr.length; i++ ){
                    if( i == startCircleIndex ){
                        continue;
                    }
                    // 檢查過就不處理了,比如當前 uncheckedMinIndex 節點是未檢查狀態,則此 for循環結束後則是 已檢查狀態,循環時就不會處理它了
                    if( distanceObjArr[ i ].checked ){
                        continue;
                    }
                    if( i == uncheckedMinIndex ){
                        continue;
                    }
                    var  tryMinDistance = distanceObjArr[ uncheckedMinIndex ].distance + graph[ uncheckedMinIndex ][ i ];
                    if( tryMinDistance < distanceObjArr[ i ].distance ){
                        distanceObjArr[ i ].distance =  tryMinDistance;
                        distanceObjArr[ i ].path = distanceObjArr[ uncheckedMinIndex ].path + i + ",";
                    }
                    if( distanceObjArr[ i ].distance < minDistance ){
                        minDistance=distanceObjArr[ i ].distance;
                        nextUncheckedMinIndex = i;
                    }
                }
                distanceObjArr[ uncheckedMinIndex ].checked=true;
                uncheckedMinIndex=nextUncheckedMinIndex;
                printDistanceObjArr( distanceObjArr );
            }
            // V4 到 v6 的最短距離是11,路線是0,4,5,6,
            console.log( "V" + startCircleIndex + " 到 v" + endCircleIndex + " 的最短距離是"
                         + distanceObjArr[ endCircleIndex ].distance 
                         + ",路線是"
                         + distanceObjArr[ endCircleIndex ].path );
            // 最短路徑標註紅色
            var trailIndexArr = distanceObjArr[ endCircleIndex ].path.split( "," );
            var prevTrailIndex = -1;
            var currTrailIndex = -1;
            for( var i=0;i<trailIndexArr.length;i++ ){
                var trailIndex = trailIndexArr[ i ];
                if( !trailIndex || trailIndex.length == 0 ){
                    continue;
                }
                trailIndex = parseInt( trailIndex );
                if( prevTrailIndex == -1 ){
                    prevTrailIndex = trailIndex;
                    continue;
                }
                currTrailIndex = trailIndex;
                // 畫連線
                drawConnectionTwoCircle( prevTrailIndex,currTrailIndex,"red",graph[ prevTrailIndex ][ currTrailIndex ] );
                prevTrailIndex = currTrailIndex;
                currTrailIndex = -1;
            }
        }
        startCircleIndex = -1;
        endCircleIndex = -1;
    }

    function printDistanceObjArr( distanceObjArr ){
        for( var i = 0;i<distanceObjArr.length;i++ ){
            if( distanceObjArr[ i ] ){
                console.log( "checked:" + distanceObjArr[ i ].checked + ", path:" +  distanceObjArr[ i ].path + ", distance:" +  distanceObjArr[ i ].distance );
            }
        }
        console.log( "" );
    }
</script>
</html>

​

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章