TWaver初學實戰——基於HTML5的交互式地鐵圖


每天坐地鐵,經常看地鐵圖,有一天突然想到,地鐵圖不也是一種拓撲結果嗎?TWaver到底能與地鐵圖擦出怎樣的火花呢?

 
想到就干,先到網上找幅參考圖。各種風格的地鐵圖還挺多,甚至有大學生自主設計制作,受到地鐵相關人士的認可和贊揚。不過看到他花了3周時間,我就比較同情他了,如果學會了TWaver,我保他連3天都不用就可以完成,而且還是純矢量、可交互、有動態效果、無失真縮放的拓撲圖。
 

我們就以上面這幅地鐵圖為模版來進行制作。

 

一、數據整理

 
俗話說兵馬未動糧草先行,沒有數據再好的創意也白搭。
 
數據格式,自然首選JavaScript原生支持的json文件,直觀方便。
 

1. 數據結構

 
數據結構是整理數據的重中之重,一個好的結構設計會讓后面的編程輕松方便。一種很容易想到的結構是以線路為基礎,每條線路依次為各個站點,但是這里面有許多站點存在多線路共用的情況,如何復用就很麻煩。另一種是以站點為基礎,再為每個站點添加線路屬性,但這樣線路的站點次序不夠清晰,在程序中很難對線路進行遍歷和循環操作。
 
那么比較好的辦法,就是將線路和站點分開,這樣將來無論是對站點還是對線路進行操作,都會比較方便。
 
{
	"stations":{
		"l01s01":{ },
		…………
	}
	"lines":{
		"l01":{……},
		…………
	}
	"sundrys":{
		"railwaystationshanghai":{……},
		…………
	}
}

 

其中第3部分“sundrys”,是需要在圖中標識的火車站、飛機場等相關元素。
 
當然,大家看到網上例子,有的會把label也單獨出來,這樣雖然可以靈活定義label 的位置,但卻使得站點和label兩張皮,而且也增加了數據采集的工作量。TWaver有對label豐富的自定義功能,所以完全沒有必要將label單拎出去,只需給其一個位置屬性就可以了。
 

2. 站點數據

 
每個站點,首先要有個屬性名。屬性名是由6位字符組成的,是由最先經過此站的線路名與站點在此線路上的序號組合而成。例如“l01s01”,表示1號線第1個站點。站點的“id”,與站點屬性名完全一致。站點的“label”屬性,是站點顯示名字相對站點的位置。
 
"l01s01":{
	"id":"l01s01",
	"name":"莘庄",
	"loc":{"x":419,"y":1330},
	"label":"bottomright.bottomright",
},
…………

 

3. 線路數據

 
線路屬性名是3位字符組成。首字符為線路類型:普通線路以“l”開頭,支線以“b”開頭,延伸線以“e”開頭,磁懸浮以“m”開頭。后兩位數字為線路的序號。線路的“id”,與線路屬性名完全一致。線路的“stations”屬性,包含了此線路上的所有站點,不過不要以為各站點的屬性名和屬性值都是一樣的,各站點的屬性名是嚴格按照線路中的順序命名的,但屬性值卻是站點的id。比如人民廣場站,其id為“l01s13”,但其在不同線路中的屬性名可能分別是“l01s13”、“l02s11”、“l08s16”。這樣既確保了對線路操作的方便性,又實現了對換乘站點的復用。
 
"l01":{
	"id":"l01",
	"name":"1號線",
	"color":"#e52035",
	"stations":{
		"l01s01":"l01s01",
		"l01s02":"l01s02",
		……
	}
},
……

 

4. 雜項數據

 
除了站點和線路以外的其他需要展示的元素都可以放到雜項數據中。雜項屬性名盡量完整表達此項目的名稱。雜項的“sign”屬性,是顯示圖標的注冊名稱。雜項的“station”屬性,是其臨靠的地鐵站id。雜項的“offset”屬性,是其顯示圖標相對地鐵站的方位。
 
"airporthongqiao":{
	"sign":"airport",
	"station":"l02s20",
	"name":"虹橋國際機場",
	"offset":{"x":0, "y":-1}
},
……

 

二、站點創建

 
地鐵線路就是一個拓撲網絡,那么站點也就是網絡的節點,創建站點也就是新建Node的過程。
 

1. 文件導入

 
所有的數據都存放在json文件中,首先要能夠讀取進來。
 
function loadJSON(path,callback){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
               dataJson = JSON.parse(xhr.responseText);
               callback && callback();
           }
       }
   };
   xhr.open("GET", path, true);
   xhr.send();
}

 

因為讀取文件是一個異步的過程,所以要程序的展開都要放在文件讀取函數的內部。

 

function init(){
    loadJSON("shanghaiMetro.json", function(){
        initNetwork(dataJson);
        initNode(dataJson);
    });
}

 

2. 站點初創

 
開始我們先不管站點的類型,對所有站點進行一次遍歷,將站點的基本信息添加到站點Node中。有心人會發現這里沒有直接設定Node的位置,而只是將位置信息存到了“location”自定義屬性中,這是因為以后統一定位可以避免由於image大小不同等原因造成的位置偏移。
 
for(staId in json.stations){
    var station = json.stations[staId];
    staNode = new twaver.Node({
        id: staId,
        name: station.name,
        image:'station',
    });
    staNode.s('label.color','rgba(99,99,99,1)');
    staNode.s('label.font','12px 微軟雅黑');
    staNode.s('label.position',station.label);
    staNode.setClient('location',station.loc);
    box.add(staNode);
}

 

3. 站點分類

 
站點主要有3種不同的類型:普通站點、換乘站點、支線共用站點。換乘站和共用站的區別,是換乘站在不同的線路中,一般並不是同一個空間,車跑的也完全不是同一個線路;而共用站卻完全是同一個地點,車也在同一條線路上跑,但不同時間跑的車可能是不同支線的車。不過對於始發或終到的共用站點,一般也都作為換乘站處理。
 
對於不同站點的判斷,無需在原始數據中指定,完全可以通過邏輯判斷來設定:只在某一條線路中出現的就是普通站點;僅在支線中重復出現的就是支線共用站點;在非支線的不同線路中重復出現的就是換乘站點。
 
最后,對不同的類型,用不同圖標顯示出來,讓用戶一目了然分辨站點。
 

4. 顯示站名

 
由於地鐵線路交錯復雜,站名的顯示位置就變得非常重要,如果不進行判斷和設置,很有可能會造成遮擋重疊,畫面會非常難看。這也是有些程序員甚至將其獨立於站點之外,作為單獨的網元重新統計創建的原因。在TWaver中可以很方便地定義label的顯示位置,甚至可以調整其顯示的角度和距離。當然可以通過程序,對站點周圍的空間進行判斷,智能調整顯示位置。但是由於有些地方過於密集和復雜,邏輯判斷的難度會非常大,不如直接在數據中手動添加位置信息來的方便。
 

5. 站點圖標

 
按照站點的分類,設計了三種不同的站點圖標,與參考地鐵圖相比,增加了支線共用站圖標。換乘站圖標沒有選擇參考地鐵圖的長方形,而是采用了更為靈活簡便的圓形圖標,省卻了方向和旋轉,方便了程序設計。
 

 

三、線路設計

 
地鐵線路由TWaver的Link實現,具有豐富的定制功能,完全可以滿足不同情況下線路顯示的需求。
 

1. 連接站點

 
對數據文件中的各條線路進行遍歷,再對每條線路中的各個站點進行遍歷,在站點間依次創建Link,基本的地鐵圖就呈現出來了。
 
for(lineId in json.lines) {
    ……
    for(staSn in line.stations) {
        ……
        var link = new twaver.Link(linkId,prevSta,staNode);
        link.s('link.color', line.color);
        link.s('link.width', linkWidth);
        link.setToolTip(line.name);
        box.add(link);
    }
}

 

可能有的地鐵圖也就到此為止了,基本的示意功能已經具備了嘛。但追求完美的TWaver怎么可能忍受,起碼線路走向要規整一些,不能兩個站點間直線一連就完事了。
 

2. 連線分型

 
觀察參考地鐵圖,是對線路進行了美化,基本只保留了橫平豎直和正斜的走向。這就需要對一些站點間的連線,加上必要的拐點,使得連線始終按照橫、豎和正斜的方向來走。
 
參考地鐵圖中,拐點的添加有時比較隨意。在一段路徑上,有的只添加一個拐點,有的又會添加兩個拐點,規律性不是很強,用程序很難模仿。
 

 

為了方便程序實現,這里最多只在相鄰兩個站點間添加一個拐點,以達到使線路方向只有直或正斜的效果。這樣的話,所有的連線就只有無拐點和有拐點兩種了。其中,無拐點連線,包括橫向、縱向、正斜向(與x軸夾角45°或-45°)三種;有拐點連線又可分為先直后斜和先斜后直兩種。
 

3.智能拐點

 
首先我們要找到需要添加拐點的連線,這個很簡單,只需要把斜率不是1的斜線找出來就可以了。下一步才是關鍵,就是判斷拐點類型,是先直后斜還是先斜后直。
 
添加拐點的一個原則,就是拐點前后,要盡量保持平直;如果必須產生夾角,也要選夾角更大的,這樣整理后的路線,才比較美觀,不會有過多不合理的轉角。
 
var setTrunType = function(json){
    box.forEach(function (ele) {
        var id = ele.getId();
        if(ele instanceof twaver.Link){
            var link = ele;
            var f = link.getFromNode().getCenterLocation();
            var t = link.getToNode().getCenterLocation();
            if(needAddPoint(f, t)){
                var so=0, os=0;
                if(link.getClient('prevLink')){
                    so += byPrevPoint(f,t,link).so;
                    os += byPrevPoint(f,t,link).os;
                }
                if(link.getClient('nextLink')){
                    os += byNextPoint(f,t,link).os;
                    so += byNextPoint(f,t,link).so;
                }
                p = os>so ? obliqueStraight(f, t) : straightOblique(f, t);
                link.setClient('point', p);
                link.setClient('truntype', os>so?'os':'so');
            }
        }
    });
}

 

 

4. 人工拐點

 
上面考慮的智能拐點的添加,但其也有局限性,不夠靈活,碰到比較復雜的情況就招架不住了。比如磁懸浮線,只有始發和終到站,而且線路比較長,只添加一個拐點無法反映真實情況,這時就必須可以人工添加多個拐點了。
 
人工拐點需要在數據中添加拐點的位置信息,然后在連線上添加拐點。人工拐點可以用setLinkPathFunction方法,但在與智能拐點混用的情況下,智能拐點判斷就比較麻煩。還有一種思路,就是將人工拐點設成一個隱形的節點,實現起來就非常容易了。
 
var createTurnSta = function(line, staSn){
    staTurn = new twaver.Node(staSn);
    staTurn.setImage();
    staTurn.setClient('lineColor',line.color);
    staTurn.setClient('lines',[line.id]);
    var loc = line.stations[staSn];
    staTurn.setClient('location',loc);
    box.add(staTurn);
    return staTurn;
}

 

 

5.接點偏移

 
地鐵圖中,有些路段是兩條線路並行的。在某些線路交叉的地方,有時甚至會在局部出現多條線段並行的情況。如果不進行設計和處理,要么多條線會重合在一起,只能顯示出其中的一條;要么兩條線會隨意分合,線路在站點處出現不美觀的彎曲。
 
當然有多種思路來解決這個問題,本例中是采取了虛擬站點的辦法。就是在站點的旁邊,添加一個Follower(但並不顯示出來),讓並行的不同線路連接到不同的Follower上。通過調整Follower的位置,就可以完美顯示線路的並行效果了。
 
var createFollowSta = function(json, line, staNode, staId){
    staFollow = new twaver.Follower(staId);
    staFollow.setImage();
    staFollow.setClient('lineColor',line.color);
    staFollow.setClient('lines',[line.id]);
    staFollow.setHost(staNode);
    var az = azimuth[staId.substr(6,2)];
    var loc0 = json.stations[staId.substr(0,6)].loc;
    var loc = {x:loc0.x+az.x, y:loc0.y+az.y};
    staFollow.setClient('location',loc);
    box.add(staFollow);
    return staFollow;
}

 

 

當然具體到每條線路在某個站點怎么偏移,很難用程序智能判斷和調整(希望有高手可以用簡潔的方式實現)。本例是手動修改線路數據,在站點的原id后添加了方位代碼。比如原為l01s11的站點,在某條線路中將其改為l01s11tt,就實現了該線路在站點頂部經過效果。具體方位代碼定義如下:
 
var azimuth = {
    bb: {x: 0, y: linkWidth*zoom/2},
    tt: {x: 0, y: -linkWidth*zoom/2},
    rr: {x: linkWidth*zoom/2, y: 0},
    ll: {x: -linkWidth/2, y: 0},
    br: {x: linkWidth*zoom*0.7/2, y: linkWidth*zoom*0.7/2},
    bl: {x: -linkWidth*zoom*0.7/2, y: linkWidth*zoom*0.7/2},
    tr: {x: linkWidth*zoom*0.7/2, y: -linkWidth*zoom*0.7/2},
    tl: {x: -linkWidth*zoom*0.7/2, y: -linkWidth*zoom*0.7/2},
    BB: {x: 0, y: linkWidth*zoom},
    TT: {x: 0, y: -linkWidth*zoom},
    RR: {x: linkWidth*zoom, y: 0},
    LL: {x: -linkWidth, y: 0},
    BR: {x: linkWidth*zoom*0.7, y: linkWidth*zoom*0.7},
    BL: {x: -linkWidth*zoom*0.7, y: linkWidth*zoom*0.7},
    TR: {x: linkWidth*zoom*0.7, y: -linkWidth*zoom*0.7},
    TL: {x: -linkWidth*zoom*0.7, y: -linkWidth*zoom*0.7}
};

 

四、動態顯示

 
TWaver做出的圖,可不是一張死圖,而是能呈現許多動態效果的生動的活的圖片。
 

1. 文本提示

 
動態鼠標提示,是TWaver的基本功能。每一個網元,不管是節點還是連線,只要設置了name屬性,鼠標移入后,默認都會以彈窗的方式將name顯示出來。當然,用戶也可以定制彈窗顯示的內容。比如,我們可以把某個站點首班和末班車的時間顯示出來,也可以把換乘信息等顯示出來,只需要一個setToolTip就可以了。
 
 

2. 站點顯示

 
當鼠標移入站點的時候,我們希望站點能有所變化,以給出動態提示。這是通過在注冊站點矢量圖形時,加入動態判斷實現的。以下代碼是普通站點的矢量圖形:
 
twaver.Util.registerImage('station',{
    w: linkWidth*1.6,
    h: linkWidth*1.6,
    v: function (data, view) {
        var result = [];
        if(data.getClient('focus')){
            result.push({
                shape: 'circle',
                r: linkWidth*0.7,
                lineColor:  data.getClient('lineColor'),
                lineWidth: linkWidth*0.2,
                fill: 'white',
            });
            result.push({
                shape: 'circle',
                r: linkWidth*0.2,
                fill:  data.getClient('lineColor'),
            });
        }else{
            result.push({
                shape: 'circle',
                r: linkWidth*0.6,
                lineColor: data.getClient('lineColor'),
                lineWidth: linkWidth*0.2,
                fill: 'white',
            });
        }
        return result;
    }
});

 

 

3. 站點動畫

 
在換乘站圖標中,還實現了旋轉的動態效果,這對於來說TWaver也很容易,只不過對rotae屬性進行了動態改變而已。
 
twaver.Util.registerImage('rotateArrow', {
    w: 124,
    h: 124,
    v: [{
        shape: 'vector',
        name: 'doubleArrow',
        rotate: 360,
        animate: [{
            attr: 'rotate',
            to: 0,
            dur: 2000,
            reverse: false,
            repeat: Number.POSITIVE_INFINITY
        }]
    }]
});

 

另外,本例還實現了站點selected和loading的動畫效果,方法都是大同小異的。
 
  
 

五、交互功能

 
交互功能是TWaver的精髓,如果只是為了圖畫的漂亮,那完全可以選擇其他作圖工具了。
 

1. 拖拽回彈

 
為了判斷是不是一張死圖,大家往往會下意識地去拖拽站點,看看能不能拖動。既然我們做的不是一張死圖,當然要讓站點能夠拖動。但如果站點會被隨便拖走,那么很快整個地鐵圖就會變得亂七八糟了,所以在松開鼠標后站點必須還能回到原來位置。
 
 
要說這個功能有什么用,我也只能呵呵了。但無聊的時候可以隨便玩上幾十分鍾我也是信的。
 

2. 混合縮放

 
既然是矢量圖,當然可以實現無失真縮放。TWaver還實現了綜合物理縮放和邏輯縮放優勢的混合縮放模式:在放大時使用邏輯縮放,更好展現站點邏輯關系;縮小時使用物理縮放,避免圖形失真。當然還有縮小后文字自動隱藏等貼心小功能,就不一一列舉了。
 
network.setZoomManager(new twaver.vector.MixedZoomManager(network));
network.setMinZoom(0.2);
network.setMaxZoom(3);
network.setZoomVisibilityThresholds({
    label : 0.6,
});

 

 

3. 經過路線

 
連續單擊同一站點(注意不是雙擊),可以將經過此站點的所有線路突出顯示出來。
 
 

4. 路徑規划

 
連續單擊不同的兩個站點,則自動規划兩站之間的合理路徑。
 
 

5. 電子地圖

 
一張地鐵圖,即使做的再復雜,功能也是有限的,有時候調用其他軟件是擴展功能的一個好辦法,比如雙擊站點后顯示站點周圍的電子地圖。
 
network.addInteractionListener(function(e){
   if(mapDiv){
        mapDiv.style.display = 'none';
        mapDiv = null;
        dbclickSta = null;
    }
    if(e.kind == 'doubleClickElement' && e.element && e.element.getClassName() == 'twaver.Node' && e.element.getId().length == 6){
        dbclickSta = e.element;
        if(dbclickSta.getClient('coord')){
            coord = dbclickSta.getClient('coord');
            mapDiv = createMap(coord, e.event);
        }else{
            dbclickSta.setClient('dbclick', true);
            var lineName = json.lines[dbclickSta.getId().substr(0,3)].name;
            var stationName = dbclickSta.getName();
            var addr = "上海市地鐵" + lineName + stationName;
            var geocoder = new qq.maps.Geocoder();
            geocoder.getLocation(addr);
            geocoder.setComplete(function(result) {
                coord =  result.detail.location;
                mapDiv = createMap(coord, e.event);
                dbclickSta.setClient('dbclick', false);
            });
            geocoder.setError(function() {
                var coord = {"lat":31.188,"lng":121.425};
                mapDiv = createMap(coord, e.event);
            });
        }
    }
});

 

在電子地圖中定位站點,可以通過在站點數據中加入站點的經緯度,也可以通過站點關鍵字在電子地圖中直接查詢。
 
 
當然,TWaver能實現不僅僅是例子中展示的這一點點,只有你想不到,沒有你做不到。你完全可以賦予地鐵圖更強大的功能,也可以舉一反三做出高鐵圖、交通圖等等類似實例。
 
 
(需要源碼的可私信索取)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM