航油

项目背景

需要展示中国地图和世界地图,并且在地图上标点,画线路图,然后需要点击效果。
地图需要有拖拽和缩放的功能。
展示的设备为 ipad 只需要适配一个固定的pad设备。

技术栈

d3/react

项目历程

项目刚开始只是先用d3+jq试验了一下,以前用d3做个树状图没做过地图,把基本的功能弄出来后,就找了个react的脚手架来开始使用react来开发,github上找到一个react-d3但是遗憾的是没有地图的封装,然后就自己半d3半react的开始开发了。
开发的时候svg的知识很重要,遇到解决的难题多搜搜svg的相关知识。
记录一下开发过程遇到的坑,地图的数据是在网上找好的,开发过程中参考了一个大神的博客,包括地图的显示,标点,线路图,拖拽和缩放功能的实现。

拖拽

开发前期用时最长的就是拖拽的功能,地图的拖拽和circlerect的拖拽不一样,他们有具体的坐标来规定显示的位置,而地图是生成的path,外层套一个g,你在调用drag事件的时候可以获取到鼠标当前的坐标,但是你给g设置了坐标不会起作用,所以选择了一个给外层的g设置translate,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const dragListener = d3.behavior.drag() //开启拖拽行为
.on('dragstart', function(d,i){//开始拖拽
d3.event.sourceEvent.stopPropagation();
})
.on('drag', dragmove);//拖拽过程

function dragmove(d,i){
x = d3.event.x - startX,
y = d3.event.y - startY;

d3.select(this)
.attr("transform",function(d,i){
return `translate(${x}, ${y})`;
});
}

第一次做的时候是在拖动开始的时候,获取鼠标的位置设置给 startX, 然后在拖动过程中,获取当前鼠标的位置再减去开始位置,设置给外层的g,这样做的话在拖动开始的时候会有一个卡顿,这是因为你在拖动开始的时候,获取的坐标会有一个偏差,最终你获取到的坐标不是你最开始的时候想要获取的,解决方法就是监听鼠标按下事件,在鼠标按下的时候获取坐标,然后就可以了。

1
2
3
4
5
6
g.attr('transform', "translate(0,0)")
.on('mousedown', function(){
startX = d3.mouse(this)[0];
startY = d3.mouse(this)[1];
})
.call(dragListener);

在pc端可以了,但是在pad上的时候出现了问题,pad上需要绑定触摸事件,后来找到了一个很棒的代替拖拽的事件,zoom缩放事件,自带拖拽功能,如果是整体拖拽的话缩放事件完全可以。

缩放

缩放事件和拖拽差不多,在你开启缩放事件后,d3会自动记录下你当前缩放的参数,

1
2
3
4
5
6
translate_x = d3.event.transform.x;
translate_y = d3.event.transform.y;
scale_cur = d3.event.transform.k;
//设置拖拽的参数
d3.select('.group').attr("transform",
`translate(${translate_x},${translate_y})scale(${scale_cur},${scale_cur})`);

缩放事件还有一个scaleExtent([1,10])方法可以设置放到最大和最小的级别
完整的代码

1
2
3
4
5
6
7
8
9
10
11
const zoom = d3.zoom()
.scaleExtent([1,10])
.on('zoom',()=>{
translate_x = d3.event.transform.x;
translate_y = d3.event.transform.y;
scale_cur = d3.event.transform.k;
d3.select('.group').attr("transform",
`translate(${translate_x},${translate_y})scale(${scale_cur},${scale_cur})`);
});

d3.select('.rootSvg').call(zoom);

关键的是最后调用的这个zoom的元素,调用zoom的元素不是包裹path的g,而是最外层的svg,这样缩放行为才能实现。

1
2
3
4
5
<svg class="rootSvg">
<g class="group">
...
</g>
</svg>

svg比canvas方便的一点就是我可以用操作dom的方式来操作我画出来的图形,我还可以给我的图形加class,然后通过class来设置图形的显示

地图

d3.js绘制地图需要用到它封装好的投影功能,项目中使用的墨卡托投影

1
2
3
4
5
6
const projection = d3.geoMercator() //设置地图的二维投影
.center(center) //设置地图的中心点
.scale(scale) //设置地图的初始缩放级别
.translate([viewWidth/2, viewHeight/2]); //设置地图平移距离
const path = d3.geoPath() //地理路径生成器
.projection(projection);

几个关于projection 和 path 的应用技巧

  • projection 可以将地图的坐标 转换为像素坐标并且在地图上显示出来
    利用这个可以在地图上渲染出点,添加图片或者显示文字都可以
    1
    2
    3
     //d.cp是点坐标数组,经度在前,纬度在后
    //经度转换为x坐标
    projection(d.cp)[0];
  • path 可以获取中心,也可以渲染geoJson数据直接渲染成path图形,比如说渲染出中国地图,部分代码如下
    利用这个可以直接生成地理路径,
    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
    g.selectAll("path")
    .data(geoData) //geoJson的数据
    .enter() //遍历所有数据
    .append('g') //把所有的path都添加到这个g中
    .attr('class', function(d){ //设置g的class
    return d.properties.ename || d.properties.name;
    })
    .append('path') //在g中添加path
    .attr('stroke',"#858384")//path描边
    .attr('stroke-dasharray',"5,5")//path描虚线
    .attr('stroke-width', 1)//path描边的宽度
    .attr("fill",fillColor)//path填充的颜色 各个省份的填充颜色 可以通过class设置 fill
    .attr('fill-opacity','.8')
    .attr('d', function(d){
    return path(d); //通过path函数直接渲染出路径图形来 d必须是geoJson格式的数据
    });
    if(fillColor == "none"){
    g.append('g')
    .attr('class','townName')
    .selectAll("text")
    .data(geoData)
    .enter()
    .append('text')
    .attr('transform',d => {
    //path.centroid(d)可以获取路经的中心点,比如说获取山西省的中心点,然后把text平移到中心点,显示省份名称
    return `translate(${path.centroid(d)[0]},${path.centroid(d)[1]})`
    })
    .text(d =>{
    if( d.properties.ename == 'taiwan'){
    return ''
    }else if(!(/[区,县]/g).test(d.properties.name)){
    return d.properties.name;
    }else{
    return ''
    }
    })
    .attr('font-size','10px')
    .attr('fill','#A9A9A9');
    }

渲染出地图来了,然后说一下地图的数据

地图数据的获取

这里有俩种数据格式,geoJsontopoJson的数据格式,topoJson的数据小,但是需要引入import * as topojson from 'topojson';然后在渲染的时候调用topojson提供的方法把topojson格式的数据转换成geojson的数据

1
2
3
4
//china_top.json 是由 china_geo.json 文件转换成的 topojson
chinaData = require('./../data/common/china_top.json');
//在使用数据前需要转换成 geoJson 格式的数据 进行渲染
geoData = topojson.feature(chinaData, chinaData.objects.china_geo).features;

福利网站

geoJson 和 topojson 数据转换 GeoJson 和 topojson 数据的转换
geoJson 地图数据的获取 这个网站可以获取到地图的geojson数据
地图上线路图和国家名称数据的制作 如果中国地图划分了大区显示,需要显示大区名称,或者需要在地图上显示从太原到北京的路线图,真心推荐这个网站,你可以在地图上画线 标点,然后 网站会给你自动生成数据,再然后你就可以开心通过path(d)渲染出来了,不用再苦逼的去找坐标点。
来油方式路线图
项目中最开心的事情就是用nodejs做各种处理数据的事情了,各种操作,然后在生成一个新的文件,过程简直爽的不要不要的,比如说我拿到的是中国各个省份的数据,我需要把各个省份的数据合并到一起,并且还需要给各个县市加上省份的名称,用nodejs只需要写写逻辑,运行一下代码,就可以拿到自己想要的数据。

路径增加运动点

这里就需要用到svg的一个运动图标的知识了animateMotion

1
2
3
4
5
<circle>
<animateMotion dur="3s" repeatCount="indefinite" rotate="auto">
<mpath href="pathId"></mpath>
</animationMotion>
</circle>

  • 外层可以是circle 也可以是 图片 是运动的图标
  • animateMotion 属性 dur 运动的时间
    repeatCount 重复次数
    rotate 运动时 外层运动图标的朝向
    
  • mpath 这个元素的href属性放置的是路径 path 的 id
    如果有一个已经定义好的path,那么外层的circle就会沿着这个path运动