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能实现不仅仅是例子中展示的这一点点,只有你想不到,没有你做不到。你完全可以赋予地铁图更强大的功能,也可以举一反三做出高铁图、交通图等等类似实例。
 
 
(需要源码的可私信索取)
原文地址:https://www.cnblogs.com/xiaor2/p/5940348.html