百万级别的空间数据可视化探索

1. 引言

空间数据往往很大,将其渲染在前端页面存在一定的难度

本文使用开源数据集,基于PostGIS、GeoServer、OpenLayers,探索并实验了一些百万级别的空间数据可视化方法

2. 数据说明

数据来源:SNAP: Network datasets: Gowalla (stanford.edu)

Gowalla数据集中共有6,442,890次签到,即6,442,890个点,文件大小约为376MB,文件格式为.txt

示例数据及字段:

[user] [check-in time] [latitude] [longitude] [location id]
196514 2010-07-24T13:45:06Z 53.3648119 -2.2723465833 145064
196514 2010-07-24T13:44:58Z 53.360511233 -2.276369017 1275991
196514 2010-07-24T13:44:46Z 53.3653895945 -2.2754087046 376497
196514 2010-07-24T13:44:38Z 53.3663709833 -2.2700764333 98503
196514 2010-07-24T13:44:26Z 53.3674087524 -2.2783813477 1043431
196514 2010-07-24T13:44:08Z 53.3675663377 -2.278631763 881734
196514 2010-07-24T13:43:18Z 53.3679640626 -2.2792943689 207763
196514 2010-07-24T13:41:10Z 53.364905 -2.270824 1042822

在QGIS中的可视化:

image-20230213104205218

3. 导入数据库

3.1 创建数据表

根据示例数据,可以发现原始文件Gowalla_totalCheckins.txttab符分割,属于csv格式

csv导入数据库是比较方便的,笔者这里使用pgAdmin,在Postgresql(带PostGIS扩展)里创建表,然后导入数据文件

数据表创建字段如下:

image-20230214140757226

对应的SQL语句(使用pgAdmin导出):

CREATE TABLE IF NOT EXISTS public.gowalla_totalcheckins
(
gid integer NOT NULL DEFAULT nextval('gowalla_totalcheckins_gid_seq'::regclass),
"user" double precision,
check_in_time character varying(24) COLLATE pg_catalog."default",
latitude numeric,
longitude numeric,
location_id double precision,
geom geometry(Point),
CONSTRAINT gowalla_totalcheckins_pkey PRIMARY KEY (gid)
)

加载PostGIS扩展:

image-20230213100516952

对应的SQL语句:

CREATE EXTENSION "postgis";

3.2 导入数据表

选中数据表,点击Tools下的Import/Export Data

image-20230213101317928

选择导入的文件Gowalla_totalCheckins.txt,然后设置分隔符为tab,导入即可:

image-20230213101550162

  • 笔者注:如果使用的pgAdmin不是安装在本地的,可以在选择文件时先点击...中的upload,将本地文件上传,如果文件大小被限制,可以在设置(preferences)中修改

3.3 设置Geometry

设置几何(Geometry)列:

UPDATE public.gowalla_totalcheckins SET geom = ST_SetSRID(ST_MakePoint(longitude,latitude), 4326);

创建空间索引,加快空间查询:

CREATE INDEX gowalla_totalcheckins_geom_idx ON public.gowalla_totalcheckins USING gist (geom);

3.4 查询数据

使用pgAdmin查询数据并可视化空间数据(部分):

image-20230213103706817

对应的SQL语句为:

SELECT * FROM public.gowalla_totalcheckins LIMIT 100

3.5 去除异常值

经度不属于-180至180,纬度不属于-90至90的数据,认为是异常值

查询异常值:

SELECT * FROM public.gowalla_totalcheckins WHERE latitude>90 OR latitude<-90 OR longitude>180 OR longitude<-180;

image-20230213203344913

删除异常值:

DELETE FROM public.gowalla_totalcheckins WHERE latitude>90 OR latitude<-90 OR longitude>180 OR longitude<-180;

4. 服务发布

空间数据存储数据库后,可以选择编写后端来接受请求发送数据,更一般的,是使用GIS服务器发布数据

这里是使用的GIS服务器是GeoServer

使用Geoserver将PostGIS中的签到数据发布可以参考:基于PostGIS使用GeoServer发布数据量大的GPS轨迹路线图 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)

总的来说,流程就是:

  • 添加工作区(可选)
  • 添加数据存储(PostGIS数据源)
  • 发布图层

添加工作区(可选项)

image-20230213132543883

添加数据存储,设置相应数据库参数:

image-20230213132609077

编辑参数发布图层:

image-20230213132743254

5. 数据可视化

这里使用OpenLayers 6 进行前端可视化渲染,数据选择一百万个点

5.1 基于CPU

使用OpenLayers比较传统的渲染方式,即canvas渲染,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- openlayers cdn -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = new ol.Map({
target: 'map',
layers: [],
view: new ol.View({
center: [112.644, 30.5158],
zoom: 2,
projection: 'EPSG:4326'
})
});
const vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
url: 'http://127.0.0.1:8080/geoserver/test/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=test%3AGowalla_totalCheckins&maxFeatures=1000000&outputFormat=application%2Fjson',
format: new ol.format.GeoJSON()
}),
style: new ol.style.Style({
image: new ol.style.Circle({
radius: 4,
fill: new ol.style.Fill({
color: [255, 0, 0, 0.2],
}),
})
}),
});
map.addLayer(vectorLayer);
</script>
</body>
</html>

image-20230213120833555

数据一次性传输完成,CPU占用较高,绘制时间较长,很卡顿

5.2 基于WebGL

使用WebGL的方式进行渲染:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- openlayers cdn -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = new ol.Map({
target: 'map',
layers: [],
view: new ol.View({
center: [112.644, 30.5158],
zoom: 2,
projection: 'EPSG:4326'
})
});
const vectorLayer = new ol.layer.WebGLPoints({
source: new ol.source.Vector({
url: 'http://127.0.0.1:8080/geoserver/test/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=test%3AGowalla_totalCheckins&maxFeatures=1000000&outputFormat=application%2Fjson',
format: new ol.format.GeoJSON()
}),
style: {
symbol: {
symbolType: 'circle',
size: 8,
color: 'rgb(255, 0, 0)',
opacity: 0.2,
},
}
})
map.addLayer(vectorLayer);
</script>
</body>
</html>

image-20230213121546955

数据一次性传输完成,CPU和GPU在绘制时都有占用,绘制时间较长,有一点卡顿,但比基于CPU的方式好很多

所以,渲染大数据时尽量使用WebGL的方式

5.3 数据聚合

屏幕的显示是有限的,人眼可见的范围也是有限的,不需要一次性绘制完所有数据

数据聚合,就是在不同缩放等级下将数据抽稀再渲染

使用OpenLayers中的聚合函数再使用WebGL的方式渲染:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- openlayers cdn -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map = new ol.Map({
target: 'map',
layers: [],
view: new ol.View({
center: [112.644, 30.5158],
zoom: 2,
projection: 'EPSG:4326'
})
});
const source = new ol.source.Vector({
url: 'http://127.0.0.1:8080/geoserver/test/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=test%3AGowalla_totalCheckins&maxFeatures=5000000&outputFormat=application%2Fjson',
format: new ol.format.GeoJSON()
})
const clusterSource = new ol.source.Cluster({
source: source,
})
map.addLayer(new ol.layer.WebGLPoints({
source: clusterSource,
style: {
symbol: {
symbolType: 'circle',
size: 8,
color: 'rgb(255, 0, 0)',
opacity: 0.2,
},
}
}));
</script>
</body>
</html>

image-20230213122906558

数据一次性传输完成,CPU和GPU在绘制时都有占用,绘制时间较长,缩放时有一点卡顿,CPU处理聚合函数需要一定的时间,GPU占用很低

地图平移还好,缩放时的卡顿感较强

5.4 矢量切片

GeoServer的矢量切片功能需要安装矢量切片扩展,具体可参考:

具体的矢量切片的使用步骤可以参考:

关于矢量切片工具GeoWebCache的使用可以参考:

如果遇到了OOM错误,可以调节JVM参数,详细参考:

JVM参数可以设置在start.bat里,笔者这里提高了JVM内存,默认是系统内存的四分之一:

image-20230214112030663

开始启动切片后,陷入了沉思:

image-20230214112209809

切片要12天?

OK,笔者先不切片或者切一部分,如果没有切片,GeoWebCache会在请求时进行切片并缓存

使用OpenLayers加载MVT矢量瓦片(.pbf):

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- openlayers cdn -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script>
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var layer = 'ne:gowalla_totalcheckins';
var projection_epsg_no = '4326';
var map = new ol.Map({
target: 'map',
view: new ol.View({
center: [1320666.0673815783, 2210184.8379129963],
zoom: 2
}),
layers: [new ol.layer.VectorTile({
style: new ol.style.Style({
image: new ol.style.Circle({
radius: 4,
fill: new ol.style.Fill({
color: [255, 0, 0, 0.2],
}),
})
}),
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: 'http://localhost:8080/geoserver/gwc/service/tms/1.0.0/' + layer +
'@EPSG%3A' + projection_epsg_no + '@pbf/{z}/{x}/{-y}.pbf'
})
})]
});
</script>
</body>
</html>

image-20230214124732733

数据多次传输,绘制时间较长,缩放时很卡顿

缩放时会多次请求,在缩放等级小时,数据量很大,网络传输限制,绘制很卡顿

但是,缩放等级大时,局部数据量不大,按需加载,避免一次性传输完成,绘制会很快,下图时缩放等级为12时的页面:

image-20230214125400176

所以,全局数据需要展示时,使用矢量切片并不是一个好的选择,但是展示局部数据时,按需加载的矢量切片是个不错的选择

6. 参考资料

[1]SNAP: Network datasets: Gowalla (stanford.edu)

[2]csv文件导入到PostGIS(一)_万里归来少年心的博客-CSDN博客

[3]Installing the Vector Tiles Extension — GeoServer 2.23.x User Manual

[4]Vector tiles tutorial — GeoServer 2.23.x User Manual

[5]利用GeoWebCache实现WebGIS地形图展示的缓存优化 - 李晓晖 - 博客园 (cnblogs.com)

[6]Container Considerations — GeoServer 2.23.x User Manual

posted @   当时明月在曾照彩云归  阅读(647)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示