每天坐地铁,经常看地铁图,有一天突然想到,地铁图不也是一种拓扑结果吗?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能实现不仅仅是例子中展示的这一点点,只有你想不到,没有你做不到。你完全可以赋予地铁图更强大的功能,也可以举一反三做出高铁图、交通图等等类似实例。
 
 
(需要源码的可私信索取)

TWaver初学实战——基于HTML5的交互式地铁图的更多相关文章

  1. 基于 HTML5 + WebGL 的地铁 3D 可视化系统

    前言 工业互联网,物联网,可视化等名词在我们现在信息化的大背景下已经是耳熟能详,日常生活的交通,出行,吃穿等可能都可以用信息化的方式来为我们表达,在传统的可视化监控领域,一般都是基于 Web SCAD ...

  2. 基于HTML5实现3D热图Heatmap应用

    Heatmap热图通过众多数据点信息,汇聚成直观可视化颜色效果,热图已广泛被应用于气象预报.医疗成像.机房温度监控等行业,甚至应用于竞技体育领域的数据分析. http://www.hightopo.c ...

  3. TWaver初学实战——如何在EasyUI中插入TWaver

    TWaver是一款强大的图形界面开发组件,可以很方便地集成到其他开发工具中.今天就用一个小例子来操练如何结合TWaver和EasyUI进行网页开发. 准备工作 俗话说他山之玉可以直接拿来,EasyUI ...

  4. TWaver初学实战——如何在TWaver属性表中添加日历控件?

    在日期输入框中添加日历控件,是一种非常流行和实用的做法.临渊羡鱼不如退而写代码,今天就看看在TWaver中是如何实现的.   资源准备   TWaver的在线使用文档中,就有TWaver Proper ...

  5. TWaver初学实战——如何在EasyUI中插入TWaver(续)

    上次文章虽然简单易懂,但很有些小伙伴不满意:你这TWaver和EasyUI结合,只不过生硬地把TWaver图形插进去了,数据和人家EasyUI没一毛钱关系.嘿嘿,不就是想发生关系嘛,没问题啊!咱就还用 ...

  6. 基于HTML5 的互联网+地铁行业

    前言 近几年,互联网与交通运输的融合,改变了交易模式,影响着运输组织和经营方式,改变了运输主体的市场结构.模糊了运营与非营运的界限,也更好的实现了交通资源的集约共享,同时使得更多依靠外力和企业推动交通 ...

  7. 基于 HTML5 Canvas 的交互式地铁线路图

    前言 前两天在 echarts 上寻找灵感的时候,看到了很多有关地图类似的例子,地图定位等等,但是好像就是没有地铁线路图,就自己花了一些时间捣鼓出来了这个交互式地铁线路图的 Demo,地铁线路上的点是 ...

  8. 快速开发 HTML5 交互式地铁线路图

    前言 前两天在 echarts 上寻找灵感的时候,看到了很多有关地图类似的例子,地图定位等等,但是好像就是没有地铁线路图,就自己花了一些时间捣鼓出来了这个交互式地铁线路图的 Demo,地铁线路上的点是 ...

  9. 基于HTML5的RDP访问实战

    基于HTML5的RDP访问实战 1.安装guacamole   2.下载源码   3.安装服务端 安装报错 错误   参考 http://www.remotespark.com/html5.html ...

随机推荐

  1. Python之路【第五篇】:面向对象及相关

    Python之路[第五篇]:面向对象及相关   面向对象基础 基础内容介绍详见一下两篇博文: 面向对象初级篇 面向对象进阶篇 其他相关 一.isinstance(obj, cls) 检查是否obj是否 ...

  2. 卡通风格的连连看ios游戏源码

    卡通风格的连连看游戏源码,该游戏是一款韩国人做的卡通风格的ios连连看游戏源码,源码设计的效果非常漂亮的,而且运行起来感觉也很好.1.游戏采用倒计时模式2.该游戏是一款社交游戏,需要通过faceboo ...

  3. 必须会的SQL语句(二) 创建表、修改表结构、删除表

    1.创建数据库表 --使用哪个数据库,如果不写这一句是默认的数据库,也可以用鼠标选当前数据库 use testDB --创建表    Create Table tablename    (       ...

  4. 【SMS】移动短信网关返回信息状态代码说明【China Mobile】

    1 由SMSC返回的一般结果状态报告 含义 说明 处理建议DELIVRD 消息发送成功 用户成功接收到短信 ??EXPIRED 因为用户长时间关机或者不在服务区等导致的短消息超时没有递交到用户手机上 ...

  5. HUE 忘记密码

    解决方法: 启动HUE的Shell /opt/cloudera/parcels/CDH/lib/hue/build/env/bin/hue shell from django.contrib.auth ...

  6. 有关c#装箱和拆箱知识整理

    c#装箱和拆箱知识,装箱和拆箱是一个抽象的概念. 1.装箱和拆箱是一个抽象的概念  2.装箱是将值类型转换为引用类型 : 拆箱是将引用类型转换为值类型 利用装箱和拆箱功能,可通过允许值类型的任何值与O ...

  7. ubuntu下svn使用指南

    ubuntu下安装subversion客户端: sudo apt-get install subversion subversion-tools 详细请看 http://www.subversion. ...

  8. PHPExcel导出导入excel、csv等格式数据

    <?php if(!defined('BASEPATH')) exit('No direct script access allowed'); //物资发料单明细 class Read_writ ...

  9. openSUSE13.1安装时要注意的问题(未完待续)

    1.最好用官方给的imageWriter来写镜像,不要用UltraISO来写镜像,会导致安装Kaffein包错误(:)可能也会有别的错误),后来我用imageWriter写了之后就没有在安装时报错了

  10. IE8中JSON.stringify方法对自动转换unicode字符的解决方案

    IE8内置了JSON对象,用以处理JSON数据.与标准方法的不同,IE8的JSON.stringify会把utf-8字符转码: var str = "我是程序员" var json ...