二维点注记在Cesium三维场景中的动态加载
引言
本文基于WFS服务构建空间数据动态请求,将预采样高程的点数据绘制在了三维场景中,避免了前端数据处理的卡顿以及三维地形对于点注记的遮挡,有效地优化了系统浏览的流畅度。
一、问题定位
系统在登录后约10s触发点注记的加载,存在20到30s的无响应时间,使用Chrome浏览器自带的性能分析工具生成火焰图,可以发现单个点数据网络请求时间最长约在2s,山峰注记数据渲染时间约在15s。通过进一步分析,发现渲染时间内占用时间最高的函数是Cesium.Scene.sampleHeight(固定位置地形高程采样),定位到相关代码发现这段逻辑是为了保证点注记能够绘制在已经完成加载的三维模型表面,所以需要获取注记点的场景高程值。
二、问题分析
- 网络请求时间较长是因为前端直接请求了整个GeoJSON文件,而文件的数据量相对较大。
- 渲染时间较长是因为前端需要采样注记点的地形高程,且山峰数据使用了billboard的加载形式,Cesium在加载大量图片注记时表现不是很理想。
三、解决思路
针对上述问题,考虑从两方面进行优化:
使用GIS软件于DEM上预采样点注记对应位置高程值,并基于中地地图服务器发布WFS服务,借助BoundingBox参数动态请求点注记范围,分批次加载注记数据。
四、操作流程
(一)预采样高程
首先准备数据集,包括DEM数据和点数据。我使用的dem数据是30m分辨率的ASTER Global Digital Elevation Map,这个分辨率足够用了。点数据是由甲方提供的。
1. 统一坐标系
略,请自行查找相关教程。
2. 采样
使用ArcGIS工具Extract Values to Points(采样值到点集)执行采样。(🠐放的链接是arcgis pro的文档一样用doge)得到的结果如下:
(二)地图服务发布
这里选择使用OGC标准的WFS服务是因为功能的实现需要对点要素的属性值-高程(上图中的RASTERVALU)进行查询,具体地图服务发布的教程请自行查找。
(三)构建动态请求
使用Postman进行测试,中地地图服务器的url构建如下:
http://127.0.0.1:8089/igs/rest/services/%E5%9C%B0%E5%9B%BE%E6%96%87%E6%A1%A3/WFSServer?service=WFS&version=2.0.0&request=GetFeature&outputFormat=json&maxFeatures=500&typeName=%E5%9C%B0%E5%9B%BE%E6%96%87%E6%A1%A3:t0&BBOX=左下点经度,左下点纬度,右上点经度,右上点纬度
**注意
-
不同地图服务器的WFS服务实现不同,具体url的构建请先寻找官方文档或咨询客服。
-
参数outputFormat取决于地图服务器的实现,直接请求http://127.0.0.1:8089/igs/rest/services/***/WFSServer,我们可以看到该地图服务器以xml的形式对于操作和操作参数的描述。(我这里放的是中地的演示地址请求结果http://webclient.smaryun.com:8089/igs/rest/services/Map/%E6%B9%96%E5%8C%97%E7%9C%813857/WFSServer)
-
中地的地图服务器声明了GetFeature操作对于application/json参数的支持,但实际请求时需要将参数设置为json而不是application/json。
-
typeName参数需要从http://127.0.0.1:8089/igs/rest/services/***/WFSServer中的FeatureTypeList中寻找
-
使用geosever地图服务器会更为方便,直接复制提供的url即可。
-
BBOX参数的设置需要根据自己的数据集情况测试判断(数据疏密程度、用户浏览的比例尺等),山峰数据我使用的是中心点加0.1度偏移,其余数据使用0.03度的偏移。
五、前端代码
大体的思路是给Camera绑定一个移动完成事件,如果移动距离大于阈值则请求移动后相机位置为中心的矩形框内的点注记数据并加载:
function d() {
// 获取camera坐标
var cameraPosition = r.camera.positionCartographic
window.center=[cameraPosition.longitude*(180/Math.PI),cameraPosition.latitude*(180/Math.PI)]
// 绑定相机事件
r.camera.moveEnd.addEventListener(onChangeCameraPosition)
}
// 加载注记
function m() {
// 读取已配置的注记图层项
let e = window.settingConfig.cffm.annotation;
if (!e)
return;
r.scene.globe.depthTestAgainstTerrain = !0;
const i = e.filter(n=>n.name != "道路名称" && n.name !="村级名" && n.name != "山峰");
for (let n = 0; n < i.length; n++) {
const t = i[n];
let l = {};
let label_appendix = {};
// 数据显示的距离配置
t.name.includes("区县名") && (l = {
near: 0,
far: 2.7e5
}),
t.name.includes("道路名称") && (l = {
near: 0,
far: 6e3
}),
t.name.includes("村级名") && (l = {
near: 0,
far: 6e3
}),
// 配置注记字体
t.name.includes("区县名") && (label_appendix = {
font: "30px sans-serif"
// showBackground: true,
// backgroundColor: Cesium.Color.DARKBLUE,
// backgroundPadding: new Cesium.Cartesian2(10, 10)
})
// 通用注记配置
let setting = {
geoJson: t.url,
type: t.type,
name: t.name,
label: {
readProperties: !0,
text: t.label,
labelStyle: "FILL_AND_OUTLINE",
color: t.labelColor,
outlineColor: t.outlineColor,
outlineWidth: 4,
labelOffset_x: 22,
labelOffset_y: -35,
...label_appendix
},
...l
}
// billboard的注记需要加载img
t.name.includes("山峰") && (setting.billboard={
imageUrl: U(t.img),
height: 20,
width: 20,
billboardOffset_x: 0,
billboardOffset_y: -30
})
t.name.includes("水库点") && (setting.billboard={
imageUrl: U(t.img),
height: 20,
width: 20,
billboardOffset_x: 0,
billboardOffset_y: -30
})
y(setting).then(v=>{
s.push(v)
}
)
}
}
// 相机移动事件
function onChangeCameraPosition(){
var camera = r.camera
// 读取当前的camera坐标
var cameraPosition = camera.positionCartographic
// console.log("cameraPosition", cameraPosition)
var centerNow=[cameraPosition.longitude*(180/Math.PI),cameraPosition.latitude*(180/Math.PI)]
if(!centerNow[0] || !centerNow[1]) return
// console.log("centerNow",centerNow)
// 使用曼哈顿距离比较阈值
var sumDiff = Math.abs(window.center[0]-centerNow[0])+Math.abs(window.center[1]-centerNow[1])
console.log("print diff: ", sumDiff)
// 阈值的确定需要根据自己的数据情况测试确定
if(sumDiff>0.04){
console.log("reload detected!!!")
console.log("window.centerold",window.center)
window.center = centerNow
console.log("window.centernew",window.center)
let e = window.settingConfig.cffm.annotation;
console.log("printing e: ",e)
const i = e.filter(n=>n.name == "道路名称" || n.name == "村级名" || n.name == "山峰");
console.log("道路名称、村级名、山峰i:", i)
for (let num = 0; num < i.length; num++){
const t = i[num];
var bbox;
if (t.name=="山峰"){
bbox = (centerNow[0]-0.1).toString()+','+(centerNow[1]-0.1).toString()+','+(centerNow[0]+0.1).toString()+','+(centerNow[1]+0.1).toString()
}
else {
bbox = (centerNow[0]-0.03).toString()+','+(centerNow[1]-0.03).toString()+','+(centerNow[0]+0.03).toString()+','+(centerNow[1]+0.03).toString()
}
console.log("bbox:",bbox)
P.get({
url: t.url,
params: {
service: "WFS",
version: "2.0.0",
request: "GetFeature",
outputFormat: "json",
typeName: t.wfsName,
maxFeatures: 3000,
BBOX: bbox
}
}).then((data)=>{
console.log("received geojsondata: ", data)
var setting = {
geoJson: data,
type: t.type,
name: t.name,
clampToGround: !0,
near: t.near,
far: t.far,
label: {
readProperties: !0,
text: t.label,
labelStyle: "FILL_AND_OUTLINE",
color: t.labelColor,
outlineColor: t.outlineColor,
outlineWidth: 4,
labelOffset_x: 22,
labelOffset_y: -35
}
}
t.name.includes("山峰") && (setting.billboard={
imageUrl: U(t.img),
height: 20,
width: 20,
billboardOffset_x: 0,
billboardOffset_y: -30
})
y(setting).then(h=>{
console.log("请求json数据成功")
window.temp
// 不需要动态删除
// if (window.json){
// r.dataSources.remove(window.json))
// window.json = h
// }
// else {window.json = h}
// console.log("数据已更新",r.dataSources)
},()=>{
console.log("请求json数据失败")
})
},()=>{console.log("request failed!")})
}
}
}