导读
阅读完此文,你会了解:1、常见的地理数据可视化图层及分类;2、GeoJSON编码格式;3、点的图层如何实现
4、OD弧线图层如何实现
5、热力图层如何实现
数据可视化图层
底图 vs 数据可视化图层
通过之前的文章,即GIS坐标体系、相机控制、数据源的金字塔构成以及二进制的解析,可以得到一个基础的矢量瓦片数据信息,包括:道路、水域、功能面、建筑、POI等信息,辅以前几篇文章(查看请访问本文末尾链接)提到的建筑的渲染、文字的渲染,再加上道路的渲染(有机会再讲)、功能面、水,基本可以构成一个较为完整的地理信息地图引擎。
地理信息地图
瓦片或矢量瓦片作为地图的底图,通常只用做地理信息的表达,如果想在地理信息底图的基础上绘制,就需要疯狂输出可视化图层,这种图层通常用来快速制作出如散点、轨迹、区面、热力图等地理位置相关的可视化作品。
图层示意
地图底图和可视化图层的关系,就好比 Mapbox和Deck.gl,高德和Loca.js。
数据可视化图层分类
经过与Loca、Deck.gl的分析和收集对比,我们根据数据源类型(点、线、面)以及可视化后的呈现方式,将图层大致分为了5类:
- 点类型数据图层
- 线类型数据图层
- 面类型数据图层
- 热力类型图层
- 其他数据模型图层
样式和数据定义
数据格式
数据结构的定义我们采用了GeoJSON的编码格式,GeoJSON是一种对各种地理数据结构进行编码的格式,每一条数据,都叫做一个特征(Feature),特征的几何类型包括:
1、点:Point、MultiPoint
2、线:LineString、MultiLineString
3、面:Polygon、MultiPolygon
GeoJSON特征
geometry属性用来描述几何属性,properties用来描述其他属性,例如:投影、bbox等。
更多规则可以参考GeoJSON规范文档:https://geojson.org/geojson-spec.html
样式格式
在样式格式上,我们借鉴了OpenLayers的样式规则,使用"image"字段来描述点的样式信息,"fill"字段描述(面)填充色样式,"stroke"字段来描述(面、线)的描边样式。
样式结构
样式合并策略
图层中特征样式的修改和设置,有时候会需要重新生成顶点和面,例如将一个高度为0的行政区设置为有高度值的行政区多边形。如果频繁设置样式,会造成CPU的消耗。为了规避这类问题,我们对样式设置的操作设置了优先合并,延迟更新的策略。即在同一帧内的更新样式后,不立即生效,更新样式的合并,在下一次事件循环周期的渲染前将样式绑定到数据上,渲染结束后清空更新样式。
样式更新逻辑
在实现和实践图层的时候遇到了很多坑点,每个图层都可以长篇大论一番。下面就挑一些常见的,有通识性问题的图层做一些实践上的分享。
点图层
绘制地理的点数据,是在做地理信息可视化中的基本诉求,可以用图标、图形做标注,用文字做信息描述。
尺寸单位
在二维的地图中,点数据通常都是矢量呈现方式,即点的大小为像素数,随着地图缩放,点的大小和尺寸保持不变。在三维的地图中的绘制点的诉求就会产生分歧,常见的三维场景,通常都是近大选小的,即随着地图缩放(相机远近),点会和周围物体一样有视觉上的尺寸的变化,因此在做三维场景中地图的点,需要考虑点的尺寸单位是物理单位(米),还是像素单位(px)。
渲染朝向
除了尺寸单位,还需要考虑点的朝向,二维场景中,所有的特征都是朝向屏幕的,在三维场景中,随着相机的倾斜,会有场景需要场景中的数据点随之一起倾斜,这就需要在生成点的Mesh的时候,也将朝向考虑进去。
综合尺寸单位和渲染朝向,现在就有了4种排列组合方式:
- 朝向屏幕,以像素为单位:常规需求,常见图标打点
- 朝向天空,以像素为单位:使用场景例如路的名称
- 朝向天空,以米为单位:应用场景例如扫描动画,表现物理影响范围
- 朝向屏幕,以米为单位:适合固定规模的场景,缩小时不重叠
实现方案
对于每一个点,都用4个顶点+2个三角形面来表示,其中四个顶点的坐标在CPU中计算为同一个位置,增加anchor信息辅助描述顶点的拉伸方向,增加offset信息描述点的偏移量,这样就可以在顶点着色器中,动态根据朝向和尺寸计算三角形面的具体顶点。
点面计算逻辑
朝向屏幕,以像素为单位的实现方式,与常规顶点计算类似,需要在计算完矩阵投影后,做顶点拉伸和加减偏移量。
// 朝向屏幕 + 像素单位vec2 r_anchor = m_ratation * (a_anchor * a_size);vec4 projected_position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.);gl_Position = vec4(projected_position.xy / projected_position.w + (r_anchor * a_scale + offset ) / u_resolution * 2.0, 0.0,1.0);
同理的还有朝向屏幕,以米为单位的场景,需要在最后的拉伸再乘一个当前视窗像素与物理单位的比例。
// 朝向屏幕 + 物理单位float mileScale = dist * PXSCALE; vec3 newPosition = vec3(position.xy + a_offset * u_cameraScale, position.z);vec4 projected_position = projectionMatrix * modelViewMatrix * vec4(newPosition.xyz, 1.);gl_Position = vec4(projected_position.xy / projected_position.w + (r_anchor * a_scale) / mileScale / u_resolution, 0.0,1.0);
朝向天空,以米为单位,需要先算出点的拉伸和偏移,再做投影矩阵运算。
// 朝向天空 + 物理单位vec3 offsetPosition = vec3(position.xy + (r_anchor * vec2(1., -1.) * a_scale + a_offset) * u_cameraScale, position.z);gl_Position = projectionMatrix * modelViewMatrix * vec4(offsetPosition, 1.);
朝向天空,以像素为单位的类似,也需要先计算拉伸和偏移,此外需要多计算一个视窗像素与物理单位的比例。
// 朝向天空 + 像素单位float pxScale = dist * PXSCALE * u_cameraScale;vec3 offsetPosition = vec3(position.xy + (r_anchor * vec2(1., -1.) * a_scale + a_offset) * pxScale * 2.0, position.z);gl_Position = projectionMatrix * modelViewMatrix * vec4(offsetPosition, 1.);
OD弧线图层
在地图场景中,OD线通常用来绘制起点和终点之间的某种关系,例如表示人口迁徙、航班、DDOS攻击等。绘制OD线的难点,在于如何生成一根“面线”。
等宽线计算
在WebGL中,绘制一根有粗度的线,是需要计算线的面网格的。可以通过相邻的两个点,计算法线方向,沿着法线方向,做宽度的拉伸,从而实现一根“面线”。
"面线"计算原理
如果需要朝向屏幕,按照像素单位宽,需要在着色器中动态计算线粗的拉伸值,具体的可以参考之前的文章《如何在WebGL中画一根2px的线》,此外,现在Three.js的官网扩展示例中,也提供了LineGeometry,也可以满足粗线的需求。
弧线计算
目前地理场景中弧线的计算,通常以大圆弧线居多。什么是大圆弧线呐?就是球面上两点最短的路径。在墨卡托投影完就是弧线的形状。大圆弧线路径的计算方式通常以二分法插值为主,即根据两点计算出中心点坐标,依次类推,计算出其他插值点。
二分法插值
大圆弧线也有其对应的弊端,就是在穿过北极或南极附近的线路,会表现异常。
异常的大圆弧线
除此以外,平面的OD弧线,我们还使用了二次贝塞尔曲线计算弧线的方法。两点的OD线,将第三个控制点选择在两点中心的垂线方向上,从而得到贝塞尔曲线的三个控制点。
贝塞尔控制点计算
三维场景的中的立体OD弧线也很常见,就是将弧度映射到海拔上(z轴),其插值点的计算方式也比较简单,将OD线的弧度均分N份,每份弧度的sin值可以计算出当前插值点的高度,根据弧度cos值可以推算当前插值点在OD线上的比例t,那么线段上任意一点,都可以通过(1-t) * a + t * b得到。
3D圆弧插值计算示意
应用场景
最终实现效果如下图(中央委员教育迁徙),用双色表示OD方向,黄色表示起点,白色表示终点。
中央委员教育迁徙OD图
热力图层
热力图是以颜色来表现数据强弱大小及分布趋势,在三维地图场景中,还可以借助高度来提升立体感。
实现热力图,需要做两个阶段的渲染,第一个阶段是密度的渲染,通常会用颜色的某一个通道,颜色叠加得到密度。我们使用到了点云颜色叠加高亮,利用r通道做密度的判断。需要注意的是,如果在地图缩放的过程中,想得到动态连续的热度变化,需要在视窗变化的时候对密度图持续更新。
密度图
第二阶段的渲染,其实体是一个网格平面。
热力网格wireframe
根据密度图的r通道,对应计算热力的颜色和高度值。在计算密度时,可以利用贝塞尔曲线对r做一些处理,使得在高度和颜色的变化上没有那么陡峭。
vec2 toBezier2(float t, vec2 P0, vec2 P1, vec2 P2, vec2 P3){ float t2 = t * t; float one_minus_t = 1.0 - t; float one_minus_t2 = one_minus_t * one_minus_t; return (P0 * one_minus_t2 * one_minus_t + P1 * 3.0 * t * one_minus_t2 + P2 * 3.0 * t2 * one_minus_t + P3 * t2 * t);}vec2 toBezier(float t, vec4 p){ return toBezier2(t, vec2(0.0, 0.0), vec2(p.x, p.y), vec2(p.z, p.w), vec2(1.0, 1.0));}
在热力着色时,需要两张纹理,第一张是密度图,第二章是热力渐变纹理,在片元着色器中,将密度值用做uv的s向量取色就OK啦。
热度渐变纹理
热力图
其它可视化图层的残坑点和代码细节未来有机会再逐一展开分享。
往期回顾
打造服务B端客户的酷炫3D地图可视化产品
数据源与存储计算
地图交互与姿态控制
地图文字渲染
地图建筑渲染
地图建筑建模制作与输出