前端对于地理流数据进行可视化。
地理流是一种地理对象间交互的表现形式,一般来说,地理流是单向的,如北京向上海的人口流动,而两个地理流构成了一对地理对象之间的交互,如北京-上海人口流动由北京向上海的人口流动与上海向北京的人口流动构成。由此,在一组地理对象间的地理流数据可以表示为一个OD(origin-destination)矩阵。基于一些需求,我需要将地理流数据展现在网页地图上,下面给出一个基于mapbox的原生解决方案。
正文
一. 贝塞尔曲线
贝塞尔曲线是一种使用数学方法描述的曲线,被广泛用于计算机图形学和动画中。一般来说,除起点与终点外,贝塞尔曲线拥有两个控制点,确定控制点与终末点的位置关系即可得到设想中的曲线。
能力有限我们无法直接得到一条曲线公式,故使用点集替代,再将其转化为弧段。
&emsp定义一个函数getCurvedLine
,输入ps(起始点),pe(结束点),arci(弧度),返回一个点集,表示包含500个点的贝塞尔曲线:
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
| function getCurvedLine(ps,pe,arci) { if(ps==pe){ console.log("!"); } const computeControlPoint1 = (ps, pe, arc = arci) => { const deltaX = pe[0] - ps[0]; const deltaY = pe[1] - ps[1]; const theta = Math.atan(deltaY / deltaX); const len = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)) / 2 * arc; const newTheta = theta - Math.PI / 2; return [ (ps[0] + pe[0]) / 2 - len * Math.cos(newTheta), (ps[1] + pe[1]) / 2 - len * Math.sin(newTheta), ]; } var controlpoint1=computeControlPoint1(ps,pe)
var linePoints=[ps,controlpoint1,pe];
var line = turf.lineString(linePoints);
var curved = turf.bezierSpline(line);
var curveCoordinates = curved.geometry.coordinates;
return curveCoordinates; }
|
二. 生成featureCollection
FeatureCollection 是 GeoJSON 格式中的一种,它用于组织多个地理要素(features)。其基本格式如下:
1 2 3 4 5 6 7 8 9 10 11
| { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": {...}, "properties": {...} }, ] }
|
FeatureCollection包含了地理要素的类型、几何信息与属性信息。在本项目中,使用mapbox展示弧线段。将点集转化为FeatureCollection的代码如下:
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
| function getArcFeature(lineList, valueList, arc) { var curveCoordinatesList=[]; lineList.forEach((line) => { curveCoordinatesList.push(getCurvedLine(line[0],line[1],arc)) }); const featureCollection = { type: 'FeatureCollection', features: [] };
for (let i = 0; i < curveCoordinatesList.length; i++) { const curveCoordinates = curveCoordinatesList[i]; const feature = { type: 'Feature', geometry: { type: 'LineString', coordinates: curveCoordinates }, properties: { 'value':valueList[i] } }; featureCollection.features.push(feature); } return featureCollection; }
|
其中,LineList包含多个点集,valueList是这些点集对应的地理流强度。本项目中使用的数据格式具体如下:

三. 在Mapbox上添加弧段
将填充好的featureCollection加载在map的source中。mapbox的初始化本文不再赘述。
1 2 3 4 5 6
| map1.addSource('line_flow'+drawnum, { 'type': 'geojson', lineMetrics: true, 'data': featureCollection });
|
其中,drawnum是每个featureCollection唯一标识符,需要保证每次添加时唯一标识符不同,否咋将导致冲突。
按照赋予的id将弧线作为图层添加到map中。
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
| map1.addLayer({ 'id': 'line_flow'+drawnum, 'type': 'line', 'source': 'line_flow'+drawnum, 'layout': { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-width': [ 'interpolate', ['linear'], ['get', 'value'], min, 0.5, max, 5 ], 'line-gradient': [ 'interpolate', ['linear'], ['line-progress'], 0, colorList[0], 0.4, colorList[1], 1, colorList[2] ] } });
|
在paint中使用line-width与line-gradient设置颜色与宽度的插值。添加起点和终点数据,作为点图层添加在map中,大致效果如左图所示
至此,归纳一下总体流程。首先需要将OD数据与OD点对间地理流的强度存储于数据库中,在显示时读取并生成贝塞尔曲线,将曲线作为geometry,地理流强度作为properties生成弧线类型的FeatureCollection,并显示在地图上。此外,mapbox还可以实现对于属性的筛选,这就可以帮助我们做两件事:一是根据流动强度筛选要显示的高于或地域某一阈值的地理流;二是通过单击基础的geojson面,通过其反馈的地级市名称,查询该地的流入\流出数据并显示,例如:
'filter': ['==','NAME', states[0].properties.NAME]
四. 添加动画
在展示单个城市的流入\流出时,可以适当添加动画以提升美观性。例如,通过递归调用绘制点的函数,来展示点扩散,以体现”向外传播“的视觉效果。动画的代码网上种类繁多,我作为前端开发业余爱好者的工作便不做展示。
值得注意的是,在添加新的数据时,最好将先前的source和layer清除,以减轻内存负担。动画的添加也可能会导致卡顿、丢帧等问题,故需要兼容性能,不过这个领域的工作我就完全不了解了T.T
五. 项目介绍
该项目爬取了2019、2020及2023年城市在五一期间的两种有向OD流数据:高德迁徙平台地级市间与人口流入流出数量正相关的迁徙指数数据共 889,375 条;百度指数平台地级市间PC端搜索量、手机端搜索量、网络搜索总量数据共 5,595,450 条。对比不同年份五一节假日虚(搜索数据)实(迁徙数据)两种地理流的空间格局,以得出相关结论。