这篇文章给大家介绍的文章内容是关于基于 HTML5 Canvas 的交互式地铁线路图,有很好的参考价值,希望可以帮助到有需要的朋友。
前言
前两天在 echarts
上寻找灵感的时候,看到了很多有关地图类似的例子,地图定位等等,但是好像就是没有地铁线路图,就自己花了一些时间捣鼓出来了这个交互式地铁线路图的
Demo,地铁线路上的点是在网上随便下载了一个,这篇文章记录自己的一些收获(毕竟我还是个菜鸟)以及代码的实现,希望能够帮到一些朋友。当然,如果有什么意见的可以直接跟我说,大家一起交流才会进步。
效果图

http://www.hightopo.com/demo/subway/index.html
地图稍微内容有点多,要全部展示,字显得有点小了,但是没关系,可以按照需求放大缩小,字体和绘制的内容并不会失真,毕竟都是用矢量绘制的~
界面生成
底层的 div 是通过 ht.graph.GraphView 组件生成的,然后就可以利用 HT for Web 提供好的方法,调用 canvas 画笔随便绘制就好,先来看看怎么生成底层 div:
1 2 3 | var dm = new ht.DataModel();
var gv = new ht.graph.GraphView(dm);
gv.addToDOM();
|
Salin selepas log masuk
addToDOM 函数声明如下:
1 2 3 4 5 6 7 8 9 10 11 | addToDOM = function (){
var self = this,
view = self.getView(),
style = view.style;
document.body.appendChild(view);
style.left = '0';
style.right = '0';
style.top = '0';
style.bottom = '0';
window.addEventListener('resize', function () { self.iv(); }, false);
}
|
Salin selepas log masuk
现在我就可以在这个 div 上乱涂乱画了~首先我获取下载好的地铁线路图上的点,我将它们放在 subway.js 中,这个 js 文件全部都是下载的内容,我没有做其他的改动,主要是将这些点根据线路来分分配添加到数组中,比如:
1 2 3 4 5 6 7 | 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]});
|
Salin selepas log masuk
接下来来描绘地铁线路,我声明了一个数组 lineNum,用来装 js 中所有的地铁线路的编号,以及一个 color 数组,用来装所有的地铁线的颜色,这些颜色的 index 与 lineNum 中地铁线编号的 index 是一一对应的:
1 2 | 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'];
|
Salin selepas log masuk
接着遍历 lineNum,将 lineNum 中的元素和颜色传到 createLine 函数中,根据这两个参数来绘制地铁线路以及配色,毕竟
js 文件中的命名方式也是有规律的,哪一条线路,则命名后面一定会加上对应的数字,所以我们只需要将字符串与这个编号结合即可获得 js
中对应的数组了:
1 2 | let lineName = 'Line' + num;
let line = window[lineName];
|
Salin selepas log masuk
createLine 的定义也非常简单,我的代码设置了不少的样式,所以看起来有点多。创建一个 ht.Polyline 管线,我们可以通过
polyline.addPoint() 函数向这个变量中添加具体的点,通过 setSegments 可以设置点的连接方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function createLine(num, color) {
var polyline = new ht.Polyline();
polyline.setTag(num);
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({
'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][1]*300});
if (num === '68'){
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');
dm.add(polyline);
return polyline;
}
|
Salin selepas log masuk
上面代码中添加地铁线上的点有分为几种情况,是因为 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。篇幅有限,添加地铁线上的点的代码部分我只展示添加“换乘站点”的点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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({
'label.scale': 0.05,
'label.font': 'bold 12px arial, sans-serif'
});
node.setSize(0.6, 0.6);
node.setImage('images/旋转箭头.json');
node.a('alarmColor1', 'rgb(150, 150, 150)');
node.a('alarmColor2', 'rgb(150, 150, 150)');
node.a('tpNode', true);
}
}
|
Salin selepas log masuk
所有的地铁线路以及站点都添加完毕。但是!你可能会看不见自己绘制的图,因为他们太小了,这个时候可以设置 graphView 拓扑组件上的 fitContent 函数,我们顺便将拓扑图上的所有东西不可移动也设置一下:
1 2 3 4 | gv.fitContent(false, 0.00001);
gv.setMovableFunc( function (){
return false;
});
|
Salin selepas log masuk
这下你的地铁线路图就可以显示啦~接下来看看交互。
交互
首先是鼠标移动事件,鼠标滑过具体线路时,线路会变粗,悬停一会儿还能看到这条线路的编号;当鼠标移动到“换乘站点”或“小站点”,站点对应的图标都会变大并且变色,字体也会变大,鼠标移开图标变回原来的颜色并且字体变小。不同点在于鼠标移动到“换乘站点”时,“换乘站点”会旋转。
鼠标滑动事件,我直接基于 gv 的底层 div 进行的 mousemove 事件,通过 ht 封装的 getDataAt 函数传入事件 event 参数,获取事件下对应的节点,然后就可以随意操作节点了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | gv.getView().addEventListener('mousemove', function (e) {
var data = gv.getDataAt(e);
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')) {
interval = setInterval( function () {
data.setRotation(data.getRotation() - Math.PI/16);
}, 100);
}
if (data.a('npNode')) {
clearInterval(interval);
}
expandNode(data, name);
dm.sm().ss(data);
name = data.getTag();
}
else {
dm.sm().ss(null);
name = '';
clearInterval(interval);
}
});
|
Salin selepas log masuk
鼠标悬停在地铁线路上时显示“具体线路信息”,我是通过设置 tooltip 来完成的(注意:要打开 gv 的 tooltip 开关):
1 2 3 4 | gv.enableToolTip();
if (num === '68') polyline.setToolTip('A P M');
else if (num === '60') polyline.setToolTip('G F');
else polyline.setToolTip('Line' + num);
|
Salin selepas log masuk
然后我利用右下角的 form 表单,单击表单上的具体线路,或者双击拓扑图上任意一个“站点”或者线路,则拓扑图会自适应到对应的部分,将被双击的部分展现到拓扑图的中央。
form 表单的声明部分我好像还没有解释。。。就是通过 new 一个 ht.widget.FomePane 类创建一个 form
表单组件,通过 form.getView() 获取表单组件的底层 div,将这个 div 摆放在 body 右下角,然后通过 addRow
函数向 form 表单中添加一行的表单项,可以在这行中添加任意多个项,通过 addRow
函数的第二个参数(一个数组),对添加进的表单项进行宽度的设置,通过第三个参数设置这行的高度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function createForm() {
var form = new ht.widget.FormPane();
form.setWidth(200);
form.setHeight(416);
let view = form.getView();
document.body.appendChild(view);
view.style.zIndex = 1000;
view.style.bottom = '10px';
view.style.right = '10px';
view.style.background = 'rgba(211, 211, 211, 0.8)';
names.forEach( function (nameString) {
form.addRow([
{
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);
});
}
|
Salin selepas log masuk
单击“站点”显示红色标注,双击节点自适应放置到拓扑图中央以及双击空白处将红色标注隐藏的内容都是通过对拓扑组件 gv 的事件监听来控制的,非常清晰易懂,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var node = createRedLight();
gv.mi( function (e) {
if (e.kind === 'clickData' && (e.data.a('tpNode') || e.data.a('npNode'))) {
node.s('2d.visible', true);
node.setPosition(e.data.getPosition().x, e.data.getPosition().y);
}
else if (e.kind === 'doubleClickData') {
gv.fitData(e.data, false, 10);
}
else if (e.kind === 'doubleClickBackground') {
node.s('2d.visible', false);
}
});
|
Salin selepas log masuk
注意 s(style) 和 a(attr) 定义是这样的,s 是 ht 预定义的一些样式属性,而 a 是我们用户来自定义的属性,一般是通过调用字符串来调用结果的,这个字符串对应的可以是常量也可以是函数,还是很灵活的。
最后还做了一个小小的部分,选中“站点”,则该“站点”的上方会显示一个红色的会“呼吸”的用来注明当前选中的“站点”。

“呼吸”的部分是利用 ht 的 setAnimation 函数来完成的,在用这个函数之前要先打开数据容器的动画开关,然后设置动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | dm.enableAnimation();
function createRedLight() {
var node = new ht.Node();
node.setImage('images/红灯.json');
node.setSize(1, 1);
node.setLayer('firstTop');
node.s('2d.visible', false);
node.s('select.width', 0);
node.s('2d.selectable', false);
node.setAnimation({
expandWidth: {
property: "width" ,
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;
}
|
Salin selepas log masuk
全部代码结束!
总结
这个 Demo
花了我两天时间完成,总觉得有点不甘心啊,但是有时候思维又转不过弯来,花费了不少的时间,但是总的来说收获还是很多的,我以前一直以为只要通过
getPoints().push 来向多边形中添加点就可以了,求助了大神之后,发现原来这个方法不仅绕弯路而且还会出现各种各样的问题,比如
getPoints 之前,一定要在多边形中已经有 points 才可以,但是在很多情况下,初始化的 points
并不好设置,而且会造成代码很繁琐,直接通过 addPoint 方法,直接将点添加进多边形变量中,并且还会默认将点通过直线的方式连接,也不用设置
segments,多可爱的一个函数。
还有就是因为 ht 默认缩放大小是 20,而我这个 Demo 的间距又很小,导致缩放到最大地铁线路图显示也很小,所以我在 htconfig
中更改了 ht 的默认 zoomMax 属性,记住,更改这个值一定要在所有的 ht 调用之前,因为在 htconfig
中设置的值在后面定义都是不可更改的。
相关推荐:
使用h5 canvas实现时钟的动态效果
HTML5 Canvas API制作简单的猜字游戏
Atas ialah kandungan terperinci HTML5 Canvas实现交互式地铁线路图. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!