基於 HTML5 Canvas 的交互式地鐵線路圖

前言

前兩天在 echarts 上尋找靈感的時候,看到了不少有關地圖相似的例子,地圖定位等等,可是好像就是沒有地鐵線路圖,就本身花了一些時間搗鼓出來了這個交互式地鐵線路圖的 Demo,地鐵線路上的點是在網上隨便下載了一個,這篇文章記錄本身的一些收穫(畢竟我仍是個菜鳥)以及代碼的實現,但願可以幫到一些朋友。固然,若是有什麼意見的能夠直接跟我說,你們一塊兒交流纔會進步。html

效果圖

1159588-20180304152222639-1272345319.png

http://www.hightopo.com/demo/...node

地圖稍微內容有點多,要所有展現,字顯得有點小了,可是不要緊,能夠按照需求放大縮小,字體和繪製的內容並不會失真,畢竟都是用矢量繪製的~json

界面生成

底層的 div 是經過 ht.graph.GraphView 組件生成的,而後就能夠利用 HT for Web 提供好的方法,調用 canvas 畫筆隨便繪製就好,先來看看怎麼生成底層 div:canvas

var dm = new ht.DataModel();//數據容器
var gv = new ht.graph.GraphView(dm);//拓撲組件

gv.addToDOM();//將拓撲圖組件添加進body中
addToDOM 函數聲明以下:數組

addToDOM = function(){   
    var self = this,
        view = self.getView(),   
        style = view.style;
    document.body.appendChild(view); //將組件底層div添加到body中           
    style.left = '0';//默認組件是絕對定位,因此要設置位置
    style.right = '0';
    style.top = '0';
    style.bottom = '0';      
    window.addEventListener('resize', function () { self.iv(); }, false); //窗口變化事件           
}

如今我就能夠在這個 div 上亂塗亂畫了~首先我獲取下載好的地鐵線路圖上的點,我將它們放在 subway.js 中,這個 js 文件所有都是下載的內容,我沒有作其餘的改動,主要是將這些點根據線路來分分配添加到數組中,好比:瀏覽器

mark_Point13 = [];//線路 數組內包含線路的起點和終點座標以及這條線路的名稱
t_Point13 = [];//換成站點 數組內包含線路中的換乘站點座標以及換成站點名稱
n_Point13 = [];//小站點 數組內包含線路中的小站點座標以及小站點名稱
mark_Point13.push({ name: '十三號線', value: [113.4973,23.1095]}); 
mark_Point13.push({ name: '十三號線', value: [113.4155,23.1080]}); 
t_Point13.push({ name: '魚珠', value: [113.41548,23.10547]}); 
n_Point13.push({ name: '裕豐圍', value: [113.41548,23.10004]});

接下來來描繪地鐵線路,我聲明瞭一個數組 lineNum,用來裝 js 中全部的地鐵線路的編號,以及一個 color 數組,用來裝全部的地鐵線的顏色,這些顏色的 index 與 lineNum 中地鐵線編號的 index 是一一對應的:app

var lineNum = ['1', '2', '3', '30', '4', '5', '6', '7', '8', '9', '13', '14', '32', '18', '21', '22', '60', '68'];
var color = ['#f1cd44', '#0060a1', '#ed9b4f', '#ed9b4f', '#007e3a', '#cb0447', '#7a1a57', '#18472c', '#008193', '#83c39e', '#8a8c29', '#82352b', '#82352b', '#09a1e0', '#8a8c29', '#82352b', '#b6d300', '#09a1e0'];

接着遍歷 lineNum,將 lineNum 中的元素和顏色傳到 createLine 函數中,根據這兩個參數來繪製地鐵線路以及配色,畢竟 js 文件中的命名方式也是有規律的,哪一條線路,則命名後面必定會加上對應的數字,因此咱們只須要將字符串與這個編號結合便可得到 js 中對應的數組了:echarts

let lineName = 'Line' + num;
let line = window[lineName];

createLine 的定義也很是簡單,個人代碼設置了很多的樣式,因此看起來有點多。建立一個 ht.Polyline 管線,咱們能夠經過 polyline.addPoint() 函數向這個變量中添加具體的點,經過 setSegments 能夠設置點的鏈接方式。ide

function createLine(num, color) {//繪製地圖線
    var polyline = new ht.Polyline();//多邊形 管線
    polyline.setTag(num);//設置節點tag標籤,做爲惟一標示
    
    if(num === '68') polyline.setToolTip('A P M');//設置提示信息 
    else if(num === '60') polyline.setToolTip('G F'); 
    else polyline.setToolTip('Line' + num);

    if(color) {
        polyline.s({//s 爲 setStyle 的簡寫,設置樣式
            'shape.border.width': 0.4,//設置多邊形的邊框寬度
            'shape.border.color': color,//設置多邊形的邊框顏色
            'select.width': 0.2,//設置選中節點的邊框寬度
            'select.color': color//設置選中節點的邊框顏色
        });
    }

    let lineName = 'Line' + num;
    let line = window[lineName];
    for(let i = 0; i < line.length; i++) {
        for(let j = 0; j < line[i].coords.length; j++) {
            polyline.addPoint({x: line[i].coords[j][0]*300, y: -line[i].coords[j][2]*300});
            if(num === '68'){//APM線(有兩條,可是點是在同一個數組中的)
                if(i === 0 && j === 0) {
                    polyline.setSegments([1]);
                }
                else if(i === 1 && j === 0) {
                    polyline.getSegments().push(1);
                }
                else {
                    polyline.getSegments().push(2);
                }
            }    
        }
    }

    polyline.setLayer('0');//將線設置在下層,點設置在上層「top」
    dm.add(polyline);//將管線添加進數據容器中儲存,否則這個管線屬於「遊離」狀態,是不會顯示在拓撲圖上的
    return polyline;
}

上面代碼中添加地鐵線上的點有分爲幾種狀況,是由於 js 中設置線的時候 Line68 有一個「跳躍」點的現象,因此咱們必須「跳躍」過去,篇幅有限 Line68 數組具體的聲明自行看 subway.js。函數

這裏說明一點,若是用的是 addPoint 函數,不設置 segments 時,默認將添加進的點用直線鏈接,segments 的定義以下:

  1. moveTo,佔用 1 個點信息,表明一個新路徑的起點
  2. lineTo,佔用 1 個點信息,表明從上次最後點鏈接到該點
  3. quadraticCurveTo,佔用 2 個點信息,第一個點做爲曲線控制點,第二個點做爲曲線結束點
  4. bezierCurveTo,佔用 3 個點信息,第一和第二個點做爲曲線控制點,第三個點做爲曲線結束點
  5. closePath,不佔用點信息,表明本次路徑繪製結束,並閉合到路徑的起始點

因此咱們要作「跳躍」的行爲設置 segments 爲 1 便可。

最後繪製這些地鐵線上的點,這個部分 subway.js 中也分離出來了,命名以「mark_Point」、「t_Point」以及「n_Point」開頭,我在前面 js 的展現部分有對這些數組進行解釋,你們動動中指劃上去看看。

咱們在這些點的位置添加 ht.Node 節點,當節點一添加進 dm 數據容器中時,就會在拓撲圖上顯示,固然,前提是這個拓撲圖組件 gv 設置的數據容器是這個 dm。篇幅有限,添加地鐵線上的點的代碼部分我只展現添加「換乘站點」的點:

var tName = 't_Point' + num;
var tP = window[tName];//大站點
if(tP) {//有些線路沒有「換乘站點」
    for(let i = 0; i < tP.length; i++) {
        let node = createNode(tP[i].name, tP[i].value, color[index]);//在獲取的線路上的點的座標位置添加節點
        node.s({//設置節點的樣式style
            'label.scale': 0.05,//文本縮放,能夠避免瀏覽器限制的最小字號問題
            'label.font': 'bold 12px arial, sans-serif'//設置文本的font
        });
        node.setSize(0.6, 0.6);//設置節點大小。因爲js中每一個點之間的偏移量過小,因此我不得不把節點設置小一些
        node.setImage('images/旋轉箭頭.json');//設置節點的圖片
        node.a('alarmColor1', 'rgb(150, 150, 150)');//attr屬性,能夠在這裏面設置任何的東西,alarmColor1是在上面設置的image的json中綁定的屬性,具體參看 HT for Web 矢量手冊(http://www.hightopo.com/guide/guide/core/vector/ht-vector-guide.html#ref_binding)
        node.a('alarmColor2', 'rgb(150, 150, 150)');//同上
        node.a('tpNode', true);//這個屬性設置只是爲了用來區分「換乘站點」和「小站點」的,後面會用上
    }
}

全部的地鐵線路以及站點都添加完畢。可是!你可能會看不見本身繪製的圖,由於他們過小了,這個時候能夠設置 graphView 拓撲組件上的 fitContent 函數,咱們順便將拓撲圖上的全部東西不可移動也設置一下:

gv.fitContent(false, 0.00001);//自適應大小,參數1爲是否動畫,參數2爲gv與邊框的padding值
gv.setMovableFunc(function(){
    return false;//設置gv上的節點不可移動
});

這下你的地鐵線路圖就能夠顯示啦~接下來看看交互。

交互

首先是鼠標移動事件,鼠標滑過具體線路時,線路會變粗,懸停一下子還能看到這條線路的編號;當鼠標移動到「換乘站點」或「小站點」,站點對應的圖標都會變大而且變色,字體也會變大,鼠標移開圖標變回原來的顏色而且字體變小。不一樣點在於鼠標移動到「換乘站點」時,「換乘站點」會旋轉。

1159588-20180209144029670-1869445696.gif

鼠標滑動事件,我直接基於 gv 的底層 div 進行的 mousemove 事件,經過 ht 封裝的 getDataAt 函數傳入事件 event 參數,獲取事件下對應的節點,而後就能夠隨意操做節點了:

gv.getView().addEventListener('mousemove', function(e) {
    var data = gv.getDataAt(e);//傳入邏輯座標點或者交互event事件參數,返回當前點下的圖元
    if(name) {
        originNode(name);//無論何時都要讓節點保持原來的大小
    }

    if (data instanceof ht.Polyline) {//判斷事件節點的類型
        dm.sm().ss(data);//選中「管道」
        name = '';
        clearInterval(interval);
    }
    else if (data instanceof ht.Node) {
        if(data.getTag() !== name && data.a('tpNode')) {//若不是同一個節點,而且mousemove的事件對象爲ht.Node類型,那麼設置節點的旋轉
            interval = setInterval(function() {
                data.setRotation(data.getRotation() - Math.PI/16); //在自身旋轉的基礎上再旋轉
            }, 100);
        }
        if(data.a('npNode')) {//若是鼠標移到「小站點」也要中止動畫
            clearInterval(interval);
        }
        expandNode(data, name);////自定義的放大節點函數,比較容易,我不粘代碼了,能夠去http://hightopo.com/   查看
        dm.sm().ss(data);//設置選中節點
        name = data.getTag();//做爲「上一個節點」的存儲變量,能夠經過這個值來獲取節點
    }
    else {//其餘任何狀況則不選中任何內容而且清除「換乘站點」上的動畫
        dm.sm().ss(null);
        name = '';
        clearInterval(interval);
    }
});

鼠標懸停在地鐵線路上時顯示「具體線路信息」,我是經過設置 tooltip 來完成的(注意:要打開 gv 的 tooltip 開關):

gv.enableToolTip();//打開 tooltip 的開關
if(num === '68') polyline.setToolTip('A P M');//設置提示信息 
else if(num === '60') polyline.setToolTip('G F'); 
else polyline.setToolTip('Line' + num);

而後我利用右下角的 form 表單,單擊表單上的具體線路,或者雙擊拓撲圖上任意一個「站點」或者線路,則拓撲圖會自適應到對應的部分,將被雙擊的部分展示到拓撲圖的中央。

form 表單的聲明部分我好像尚未解釋。。。就是經過 new 一個 ht.widget.FomePane 類建立一個 form 表單組件,經過 form.getView() 獲取表單組件的底層 div,將這個 div 擺放在 body 右下角,而後經過 addRow 函數向 form 表單中添加一行的表單項,能夠在這行中添加任意多個項,經過 addRow 函數的第二個參數(一個數組),對添加進的表單項進行寬度的設置,經過第三個參數設置這行的高度:

function createForm() {//建立右下角的form表單
    var form = new ht.widget.FormPane();
    form.setWidth(200);//設置表單寬度
    form.setHeight(416);//設置表單高度
    let view = form.getView();
    document.body.appendChild(view);//將表單添加進body中
    view.style.zIndex = 1000;
    view.style.bottom = '10px';//ht組件幾乎都設置絕對路徑
    view.style.right = '10px';
    view.style.background = 'rgba(211, 211, 211, 0.8)';

    names.forEach(function(nameString) {
        form.addRow([//向表單中添加行
            {//這一行中的第一個表單項
                button: {//向表單中添加button按鈕
                    icon: 'images/Line'+nameString.value+'.json',//設置按鈕的圖標
                    background: '',//設置按鈕的背景
                    borderColor: '',//設置按鈕的邊框顏色
                    clickable: false//設置按鈕不可點擊
                }
            },
            {//第二個表單項
                button: {
                    label: nameString.name,
                    labelFont: 'bold 14px arial, sans-serif',
                    labelColor: '#fff',
                    background: '',
                    borderColor: '',
                    onClicked: function() {//按鈕點擊回調事件
                        gv.sm().ss(dm.getDataByTag(nameString.value));//設置選中按下的按鈕對應的線路
                        gv.fitData(gv.sm().ld(), true, 5);//將選中的地鐵線路顯示在拓撲圖的中央
                    }
                }
            }
        ], [0.1, 0.2], 23);//第二個參數是設置第一參數中的數組的寬度,小於1是比例,大於1是實際寬度。第三個參數是該行的高度
    });
}

單擊「站點」顯示紅色標註,雙擊節點自適應放置到拓撲圖中央以及雙擊空白處將紅色標註隱藏的內容都是經過對拓撲組件 gv 的事件監聽來控制的,很是清晰易懂,代碼以下:

var node = createRedLight();//建立一個新的節點,顯示爲「紅燈」的樣式
gv.mi(function(e) {//ht 中拓撲組件中的事件監聽
    if(e.kind === 'clickData' && (e.data.a('tpNode') || e.data.a('npNode'))) {//e.kind獲取當前事件類型,e.data獲取當前事件下的節點
        node.s('2d.visible', true);//設置node節點可見
        node.setPosition(e.data.getPosition().x, e.data.getPosition().y);//設置node的座標爲當前事件下節點的位置
    }
    else if(e.kind === 'doubleClickData') {//雙擊節點
        gv.fitData(e.data, false, 10);//將事件下的節點自適應到拓撲圖的中央,參數1爲自適應的節點,參數2爲是否動畫,參數3爲gv與邊框的padding
    }
    else if(e.kind === 'doubleClickBackground') {//雙擊空白處
        node.s('2d.visible', false);//設置node節點不可見 查看 HT for Web 樣式手冊(http://www.hightopo.com/guide/guide/core/theme/ht-theme-guide.html#ref_style)
    }
});

注意 s(style) 和 a(attr) 定義是這樣的,s 是 ht 預約義的一些樣式屬性,而 a 是咱們用戶來自定義的屬性,通常是經過調用字符串來調用結果的,這個字符串對應的能夠是常量也能夠是函數,仍是很靈活的。

最後還作了一個小小的部分,選中「站點」,則該「站點」的上方會顯示一個紅色的會「呼吸」的用來註明當前選中的「站點」。

1159588-20180209151906576-1560424730.gif

「呼吸」的部分是利用 ht 的 setAnimation 函數來完成的,在用這個函數以前要先打開數據容器的動畫開關,而後設置動畫:

dm.enableAnimation();//打開數據容器的動畫開關
function createRedLight() {
    var node = new ht.Node();
    node.setImage('images/紅燈.json');//設置節點的圖片
    node.setSize(1, 1);//設置節點的大小
    node.setLayer('firstTop');//設置節點顯示在gv的最上層
    node.s('2d.visible', false);//節點不可見
    node.s('select.width', 0);//節點選中時的邊框爲0,不可見
    node.s('2d.selectable', false);//設置這個屬性,則節點不可選中

    node.setAnimation({//設置動畫 具體參見 HT for Web 動畫手冊(http://www.hightopo.com/guide/guide/plugin/animation/ht-animation-guide.html)
        expandWidth: {
            property: "width",//設置這個屬性,而且未設置 accessType,則默認經過 setWidth/getWidth 來設置和獲取屬性。這裏的 width 和下面的 height 都是經過前面設置的 size 獲得的
            from: 0.5, //動畫開始時的屬性值
            to: 1,//動畫結束時的屬性值
            next: "collapseWidth"//字符串類型,指定當前動畫完成以後,要執行的下個動畫,可將多個動畫融合
        },
        collapseWidth: {
            property: "width",
            from: 1, 
            to: 0.5,
            next: "expandWidth"
        },
        expandHeight: {
            property: "height",
            from: 0.5, 
            to: 1,
            next: "collapseHeight"
        },
        collapseHeight: {
            property: "height",
            from: 1, 
            to: 0.5,
            next: "expandHeight"
        },
        start: ["expandWidth", "expandHeight"]//數組,用於指定要啓動的一個或多個動畫
    });
    dm.add(node);
    return node;
}

所有代碼結束!

總結

這個 Demo 花了我兩天時間完成,總以爲有點不甘心啊,可是有時候思惟又轉不過彎來,花費了很多的時間,可是總的來講收穫仍是不少的,我之前一直覺得只要經過 getPoints().push 來向多邊形中添加點就能夠了,求助了大神以後,發現原來這個方法不只繞彎路並且還會出現各類各樣的問題,好比 getPoints 以前,必定要在多邊形中已經有 points 才能夠,可是在不少狀況下,初始化的 points 並很差設置,並且會形成代碼很繁瑣,直接經過 addPoint 方法,直接將點添加進多邊形變量中,而且還會默認將點經過直線的方式鏈接,也不用設置 segments,多可愛的一個函數。

還有就是由於 ht 默認縮放大小是 20,而我這個 Demo 的間距又很小,致使縮放到最大地鐵線路圖顯示也很小,因此我在 htconfig 中更改了 ht 的默認 zoomMax 屬性,記住,更改這個值必定要在全部的 ht 調用以前,由於在 htconfig 中設置的值在後面定義都是不可更改的。

總而言之,這兩天個人腦細胞死了很多,也從新生長了很多,人都是在不斷進步的嘛~

相關文章
相關標籤/搜索