zoukankan      html  css  js  c++  java
  • 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能实现不仅仅是例子中展示的这一点点,只有你想不到,没有你做不到。你完全可以赋予地铁图更强大的功能,也可以举一反三做出高铁图、交通图等等类似实例。
     
     
    (需要源码的可私信索取)
  • 相关阅读:
    黑马day07 注册案例(二)
    LeetCode--Best Time to Buy and Sell Stock (贪心策略 or 动态规划)
    让UIView窄斜
    Android Material Design-Creating Lists and Cards(创建列表和卡)-(三)
    c#为了实现自己的线程池功能(一)
    4、应用程序设置应用程序详细信息页面
    【NIO】dawn在buffer用法
    《ArcGIS Runtime SDK for .NET开发笔记》--在线编辑
    ArcGIS Runtime SDK for .NET (Quartz Beta)之连接ArcGIS Portal
    《ArcGIS Runtime SDK for .NET开发笔记》--三维功能
  • 原文地址:https://www.cnblogs.com/xiaor2/p/5940348.html
Copyright © 2011-2022 走看看