首页 > 代码库 > TWaver初学实战——制作交互式地铁图
TWaver初学实战——制作交互式地铁图
每天坐地铁,经常看地铁图,有一天突然想到,地铁图不也是一种拓扑结果吗?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. 文本提示
label显示,是TWaver的基本功能。每一个网元,不管是节点还是连线,只要设置了name属性,鼠标移入后,默认都会以label的方式将name显示出来。当然,用户也可以定制label显示的内容。比如,我们可以把某个站点首班和末班车的时间显示出来,也可以把换乘信息等显示出来,只需要一个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能实现不仅仅是例子中展示的这一点点,只有你想不到,没有你做不到。你完全可以赋予地铁图更强大的功能,也可以举一反三做出高铁图、交通图等等类似实例。
(需要源码的可私信索取)
TWaver初学实战——制作交互式地铁图
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。