地图与飞线

2016-04-21

使用 d3.js 绘制地图和飞线动效。涉及内容:GeoJSON、地图投影、贝塞尔曲线、中间帧动画、蒙板等

一、绘制地图

以绘制海南省为例:

海南省.png

1. 拿到原始数据,比如构成海南省边界的一系列经纬点。

2. 转成适合 d3 识别的格式,如 GeoJSON

  • GeoJSON 是一种专门用于描述地理数据且基于 JSON 的公开标准。
  • 如海南省边界 GeoJSON 格式如下:
海南省数据.png

3. 将经纬度地理坐标(三维)转换成平面直角坐标(二维),即 “地图投影”。

为什么称之为 “投影”?

以圆柱投影为例,假想球中心有一处光源,球体的影子印在圆柱上,再把圆柱展开。

圆柱投影.png

图片来源自 wikipedia

d3 中内置了多种地球投影函数,如 d3.geo.mercator()等,调用非常方便。

4. 生成 SVG 路径

调用 d3 中的路径生成器 —— d3.geo.path() ,生成 SVG 路径。

// mercator 投影法,即正轴等角圆柱投影
var projection = d3.geo.mercator()

// 调用路径生成器,加入投影函数,生成路径。
d3.geo.path().projection(projection);

最后将生成的 SVG 路径数值,放入<path>d属性中,即可渲染出海南省

<path d="M1016.3165908260071,877.0020828012596L1016.7682629425233,878.4541870083519L1018.1190182452278,879.657674665422L1020.6713918274147,879.9425987991278L1021.8133553115683,885.0159656142133L1021.2168072186639,886.604367509897L1019.6572600974043,888.3863507038384L1018.7027831627181,888.1210805263548L1017.5821249651053,890.4022390262162L1016.5466879311009,890.6605304316585L1014.6405747770752,895.1029959335299L1014.1903230095061,897.3390762043034L1012.4858999118553,900.5456612724538L1012.5455547246386,902.4770156266966L1011.8083917196311,904.251242336848L1008.3256872046436,904.9648477276073L1006.1624902108458,906.8575129740502L1005.2776105490134,909.6023273911044L1002.8828961011761,909.1128379773108L1001.4881098764467,909.7482614499434L999.9285627377321,909.4975500969631L998.7269444582525,912.2707386591089L996.8208312867737,913.2240027380744L994.5851963222347,911.4637932711369L990.7133151839512,911.1571154411927L989.7446347249067,911.4847360484749L988.5060872681313,909.9997012838144L986.9195534470948,910.025891775339L983.3388445930832,907.8296269799064L981.5832887937775,907.4933903587646L981.5591428093464,903.5329747367365L980.40439614978,901.3944349544332L980.864590394872,899.8235323030736L980.1913432669389,896.4899343077275L981.02651057606,895.4811777242269L980.4782544869208,893.0774565320271L981.5719259673072,891.3381575865278L982.9042166988199,891.078418535463L985.8727535866233,888.8618187772439L987.1837390315939,887.2474817087502L989.6381082922107,886.351004192087L989.8184930620489,883.4614340271595L992.2075260966478,881.3048632571645L995.0226649143901,882.9314989438175L996.1049735857605,882.4135500915461L996.1049735857605,880.7541404207199L997.3037511673499,879.9781179011286L999.455585334679,879.6531398322504L1001.0023492805256,880.7541404207199L1002.3232771855689,880.1322809066637L1004.5986820252949,880.6400545755691L1005.9238709771737,880.2577212784149L1007.0999229232812,878.599357584266L1008.9151335179147,879.2351497824575L1011.0442420323056,878.3453043291311L1014.4885970559592,880.2274951763136L1014.2968494422012,878.3687446898831Z"></path>

二、绘制飞线

1. 找到城市

原始数据

[{
    "from": {
        "name":"拉萨",
        "coordinate": [116.4551,40.2539]
    },
    "to": {
        "name":"北京",
        "coordinate": [91.1865,30.1465]
    }
}]

三维转二维坐标

与绘制地图时相似,使用 projection() ,把经纬度转为直角坐标。

2. 绘制路径

二次贝塞尔曲线

// 起始点为(50,50),控制点在(50,100),结束点为(100,100)
<path d="M50,50 Q50,100 100,100" />
飞线.png

起始点拉萨坐标,结束点北京坐标,控制点由计算得出,如下:

求控制点坐标.png

三、飞线动画

使用 attrTween(),插入中间帧函数,不断变更 <path> 中的 d 属性,呈现出线条在“一点点绘制出来”的效果。

飞线动画.png
// 过渡动画
flyline.transition()

    // 动画时长
    .duration(1800)

    // 为属性 d ,设置中间帧过渡
    .attrTween('d', function(d){

        var l = $path.getTotalLength();

        return function(t){

            var p = $path.getPointAtLength(t * l)

            return '最终返回的值'
        }
    });

说明:

  1. $path 变量为完整的飞线路径,即最终效果的飞线;
  2. getTotalLength() 得出该 <path> 的总长度;
  3. 此时的 t,即是中间帧的时刻。值范围为[0, 1],总数量大概会有 100 帧左右(为何是 100 帧左右,而不是个确切的数?暂没搞懂..)
  4. getPointAtLength() 传入路径上距离,返回该点的 x,y 坐标

新的控制点如何确定?

通过起始点和原控制点,求出新的控制点

新的控制点.gif
新的控制点计算公式.jpg

图片来源 cnblogs

取 p01 为新的控制点。

通过新控制点和终点(变量),起始点不变,动态一次次绘制飞线。

function valueTween(d){

    var $path = d3.select(this.parentNode).select('.line-basic');

    // 基路径
    var coord = $path.attr('d').replace(/(M|Q)/g, '').match(/((\d|\.)+)/g);

    var x1 = +coord[0], y1 = +coord[1], // 起点
        x2 = +coord[2], y2 = +coord[3], // 控制点
        x3 = +coord[4], y3 = +coord[5]; // 终点

    var l = $path.node().getTotalLength();

    return function(t){


        // 新的终点
        var p = $path.node().getPointAtLength(t * l);

        // 新的控制点
        var x = (1-t) * x1 + t * x2;
        var y = (1-t) * y1 + t * y2;

        return 'M'+x1+','+y1+' Q'+x+','+y+' '+p.x+','+p.y;
    }
}

四、飞线样式

1. 使用 svg 蒙板,渲染飞线“头粗尾巴细”的效果

(1) 添加圆形蒙板

蒙板.png
  • 圆心 cy,cx 为飞线终点;
  • 设置的半径即为可视区域;
  • 蒙板动态跟随飞线变化。

(2) svg 中 <mask> 标签

<defs>
    <mask id="Mask">
          <circle r="100" fill="url(#grad)"  />
    </mask>
</defs>

(3) 为蒙板添加径向渐变,使得飞线有“头部深,尾部浅至透明”的效果

<radialGradient
    id="grad"
    cx="0.5"
    cy="0.5"
    r="0.5" >
    <stop offset="0%" stop-color="#fff" stop-opacity='1' />
    <stop offset="100%" stop-color="#fff" stop-opacity='0'/>
</radialGradient>

2. 为飞线添加一个亮色的头部

飞线头部亮色.png

3. 优化

蒙板半径-before.png

原因是蒙板半径没有自适应。当半径为一个固定数值时,将导致长度小于此值的飞线没掉了尾部渐变效果。如下图,白色圆圈为蒙板范围:

mask-radius.png

优化:使蒙板半径随着两点(起点与终点)的距离而变化

mask-radius2.png

五、总结

整个流程如下:

  1. 加载地图数据,绘制出地图;
  2. 轮询飞线数据,保存在数据中心;
  3. 飞线池 FlylinePond 初始化 生成飞线实体;
  4. 启动飞线数据运输带 - 不断绘制(只要数据池中有数据)
  5. draw() -已知起点和终点,二次贝塞尔曲线
    • 绘制飞线基本路线
    • 飞线动画,不断改变 d 属性; attrTween
    • 飞线头部
    • 蒙板
    • 结束圆圈
    • 终点文字

效果图:

效果图.gif

六、资料:

  1. eChart(百度)-地图-模拟迁徙
  2. 贝塞尔曲线原理
  3. D3.js 入门系列 — 地图的制作
  4. Marker animation along SVG path element with D3.js
  5. D3.js 官网 Tutorials
  6. wikipedia: 麥卡托投影法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容