Geoserver:小程序巨丝滑渲染海量点位

文章最后有效果图

需求

在小程序上绘制 40000+ 的点位。

难点

众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callout) 会更卡,所以渲染 4w+ 的点,用常规方法是不可能实现的。

方案

按需加载

按需加载即按屏幕坐标加载,只显示视野范围内的点位,需要后端配合在接口中新增 bbox(Bounding box) 参数,再从数据库中查出范围内的点。

小程序端需要使用视野变化监听方法实时更新,虽然请求和渲染频繁,但是在缩放等级较大时,有很高的性能:

ini 复制代码
<map bindregionchange="regionChanged" markers="{{markers}}">
scss 复制代码
regionChanged(e){
    this.data.bbox = [        [e.detail.region.southwest.longitude, e.detail.region.southwest.latitude],
        [e.detail.region.northeast.longitude, e.detail.region.northeast.latitude],
    ]
    // 执行获取点、渲染点的操作
}

需要注意的是,目前的微信版本(8.0.47),基础库3.3.4该方法不可用,见 微信开放社区

如果遇到 bindregionchange 不可用时,可以用 bind:touchend 方法代替,手动获取范围

javascript 复制代码
    setBbox() {
      mapCtx = wx.createMapContext('map', this)
      mapCtx.getRegion({
        success: (res) => {
          let bbox = [
            [res.southwest.longitude, res.southwest.latitude],
            [res.northeast.longitude, res.northeast.latitude],
          ]
          // 执行获取点、渲染点的操作
         })
      })
    }

使用了按需渲染后,在缩放等级较大时,已经可以有很好的效果,移动屏幕时基本可以秒加载出新的点,同时清除掉屏幕范围外的点。

然而,在点位多的时候,我们收到了 setData 长度超出的报错,页面也异常卡顿。

优化渲染方式

小程序的 setData 方法最多只能更新 1M 的数据,超过这个数据会报错,并严重卡顿,即使不超过,在数据量较大时,也会非常卡顿,为了解决这个问题,我们不能再使用 setData 去渲染数据。

小程序提供了专门渲染点的方法: addMarkers

// 执行获取点、渲染点的操作处,使用该方法,并设置 clear: true 。这样就达到了上面说的,更新点时,旧的点会被清除。

然而,这并没有解决根本问题,我们现在可以做到渲染远远大于1M的数据,并渲染时不会报错,但是由于小程序 map 组件的渲染策略,我们的点会一个一个渲染上去,我们知道更新 canvas 代价是很大的,尤其是像 marker 这种携带很多必要信息的东西。

这里我们尝试将 marker 携带的参数压缩到极致,仅保留经纬度、颜色状态信息、id、callout,效果依然差强人意。

并且,由于小程序 marker 的 callout 不是互斥的,且没有给我们预留参数去设置这一点,所以在我们切换 marker 选中状态时,需要把 marker 数组完全遍历一遍,移除其他的 callout , 并添加新的 callout,这个开销也是巨大不可接受的。

优化选中策略

为了解决切换 marker 选中状态时的开销问题,我们想了一个绝妙的主意,就是将 marker 数组中的 callout 完全移除,只保留 id 等必要字段,在点击时,添加一个新的带 callout 点上去,盖住原来的点,这样看起来就是原有的点被选中了,这样既压缩了 marker 携带参数,又解决了切换选中时必须遍历 marker 数组的问题。

less 复制代码
height: 20,
width: 17,
iconPath: this.data.markerIcons[this.getMarkerType(item)],
latitude: item.point[1],
longitude: item.point[0],
id: this.getUniqueNumber(item.uid), //id 必须是数字
storeCode: item.uid,
//callout:{...} // 不要此项 
customCallout: {} //必须加,不然会有一个没有内容的弹窗,这个可以阻止默认弹窗弹出

优化海量点渲染策略

经过上面的优化,我们的小程序已经可以高性能的显示点位了,但是当缩放等级低时(12以下),点位多起来了,我们目前的方法就显得力不从心了。

如果点位无限多,我们又该如何优化呢?

聚合

聚合指的是将临近的点位聚合成一个大点,从而达到渲染点数变少、提高性能的方法。

此方法经过实测,发现当点达到一定量级的时候,用了反而比不用还卡,因为每当你缩放地图时,都需要计算聚合,当计算压力大于渲染压力时,聚合反而成了一种负担,而不是优化了。

所以我们不用聚合。

小程序个性化图层

小程序提供了付费功能:个性化图层,可以上传海量数据并生成一个小程序支持加载的图层。遗憾的是这种方法只适合静态数据,对于经常需要变动的数据,这种方法的实时性得不到保证,只能通过手动在后台更新数据。

所以此方案也不可用。

瓦片

小程序 map 是不支持瓦片(个性化图层除外)加载的,但是我们知道,瓦片就是一张图片而已,那么小程序可以在地图上放图片吗,答案是可以:addGroundOverlay

我们决定朝着此方案努力,请看下文。

搭建 geoserver

首先到 geoserver官网 下载geoserver本体,geoserver是为数不多几个推荐 windows 平台的大型工具软件,下载前注意,geoserver对 jdk 版本有要求,版本不一致会导致 geoserver 启动失败等问题。

我们的服务器是 linux ,所以下载了linux版本,到服务器找个位置 直接 unzip 就可以了。

安装完之后,需要先编辑 start.ini 调整一个合适的空闲端口,作为后面web端管理页面的地址端口。别忘了在防火墙开启此端口。

最后在 bin 中有一个 startup.sh , 使用 nohup 命令设置后台运行。

此时在浏览器输入服务器地址和你刚刚设置的端口号,最后加上 /geoserver,即可看到geoserver的管理页面。

初始用户名密码:admin geoserver

登录完成后可以看到全部功能

点击数据存储 -> 添加新的数据存储,即可添加数据并发布图层。

可以看到支持 PostGis,使用 PostGis 作为数据源,图层会实时更新,也就是说,当数据变化时,无需任何代码和人工干预。

当数据源添加完成后,需要新建一个图层,并指定为刚刚新建的数据源。

此时,在图层预览页面即可看到刚刚创建的图层了,当然此时的图层使用的是默认样式,需要编写SLD(xml格式)的样式文件去指定样式,这对于我们来说无疑是一种负担。

好在 geoserver 有 css 插件,安装此插件并重启geoserver,即可使用 css 编写图层样式。

css 复制代码
* {
  mark-size:8px;
}
[control_sts == 1] {
  mark:url("https://entropy.xxx.cn/xx/dotgreen.png");
}
[control_sts == 0] {
  mark:url("https://entropy.xxx.cn/xx/dotgray.png");
}

可以看到,它与标准css还是有一些差异的,像mark、mark-size在标准css中是不存在的。

指定样式后,在图层预览页面,可以看到效果

打开控制台,可以看到网络请求中的地址长这样:

perl 复制代码
http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=cite%3Axc_store_geo&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4326&WIDTH=670&HEIGHT=768&BBOX=114.4720458984375%2C37.7874755859375%2C118.1524658203125%2C42.0062255859375

放到浏览器窗口打开,发现是一张png图片,那么我们刚好可以使用小程序的 addGroundOverlay 添加到地图上。

vbnet 复制代码
SERVICE: WMS
VERSION: 1.1.1
REQUEST: GetMap
FORMAT: image/png
TRANSPARENT: true
STYLES: 
LAYERS: xx:xxxx
exceptions: application/vnd.ogc.se_inimage
SRS: EPSG:4326
WIDTH: 670
HEIGHT: 768
BBOX: 114.4720458984375,37.7874755859375,118.1524658203125,42.0062255859375

看一下这些参数,出了 BBOX ,其他的写固定值就可以了。

这里注意,宽高值,需要设置为小程序中地图元素的大小,单位是 px。

在小程序中拼装WMS地址

比较简单,直接看代码:

typescript 复制代码
    setTileImage(params: { LAYERS: string[], BBOX: string, SCREEN_WIDTH: number, SCREEN_HEIGHT: number, CQL_FILTER: string }) {
      mapCtx = wx.createMapContext('map', this)
      this.removeTileImage().then(() => {
        for (let index in params.LAYERS) {
          let id = +(9999 + index)
          !this.data.groundOverlayIds.includes(id) && this.data.groundOverlayIds.push(id)
          let data: any = {
            id: +(9999 + index),
            zIndex: 999,
            src: `http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${params.LAYERS[index]}&STYLES=&exceptions=application/vnd.ogc.se_inimage&FORMAT=image/png&TRANSPARENT=true&FORMAT_OPTIONS=antialias:full&SRS=EPSG:4326&BBOX=${params.BBOX}&WIDTH=${params.SCREEN_WIDTH * 2}&HEIGHT=${params.SCREEN_HEIGHT * 2}&CQL_FILTER=${params.CQL_FILTER}`,
            bounds: {
              southwest: {
                latitude: +params.BBOX.split(',')[1],
                longitude: +params.BBOX.split(',')[0]
              },
              northeast: {
                latitude: +params.BBOX.split(',')[3],
                longitude: +params.BBOX.split(',')[2]
              }
            }
          }
          mapCtx.addGroundOverlay({
            ...data,
          })
        }
      })
    },

我这里封装了一个可以接受多个图层的方法,这里值得注意的是,我没有使用 updateGroundOverlay 方法去更新图层,而是先使用 removeGroundOverlay 移除,再重新添加的,这是因为updateGroundOverlay有一个bug,我不说,你可以自己试试。

完成

至此已经完全实现了小程序的海量点的渲染,无论点有多少,我们都只需要渲染一张图片而已,性能好的一批。

相关推荐
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月1 小时前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天1 小时前
【Node.js]
前端·node.js
前端(从入门到入土)1 小时前
微信小程序自定义顶部导航栏(适配各种机型)
微信小程序·小程序
2401_857610031 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
雾散声声慢2 小时前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫2 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子2 小时前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog2 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪2 小时前
vue文本高亮处理
前端·javascript·vue.js