05-前端-热区识别、计算与绘制
本篇是《百万级点数据:MVT + WebGL 热力渲染与 FBO 连通域热区工单统计》系列的第五篇(末篇),聚焦前端(下):热力图画出来之后,如何把屏幕上「看起来热」的区域识别成可统计、可点击的热区。第三篇解决了「瓦片化热力怎么画」,第四篇解决了「多 LOD 下 renderer 只 composite 当前档瓦片」;本篇在同一 WebGL 热力层之上完成帧缓冲读回、连通域标注(CCL)、热区合计与质心计算、边界与圆标绘制,最终点击热区下钻到工单明细,与总览篇的端到端数据流闭环。
前端(下):热区识别、计算与绘制
背景环境
本篇依赖如下组件,其它小版本需自行回归验证。
| 项 | 版本 / 说明 |
|---|---|
| OpenLayers | 10.6.1,与第三、四篇锁定同一版本;本篇涉及的私有渲染结构使用点与该版本锁定,升级需逐项回归 |
| WebGL 热力层 | 承接第三篇构建的自定义 WebGLVectorTile 子类(splat + gradient 全屏后处理),并经第四篇 renderer active LOD 过滤;本篇所有识别都发生在这一层产出的画面上 |
1. 识别:为什么用 FBO readback,而不是格网几何聚类
先明确「热区」到底是什么。一个直觉的误区是把它当成 GIS 对象:拿到格心坐标,做 DBSCAN 之类的空间聚类,把距离相近的格网归并成区。但用户看到的热区并不是格网点的邻接关系,而是 splat 晕染之后屏幕上连成一片的亮斑——两个格网中心可能隔了几个格距,只要半径够大、权重够高,晕染在像素上就是连通的;反过来,两个相邻格网权重都极低时,视觉上也构不成「热」。
所以本方案把热区定义为像素级视觉对象:把阈值划在 GPU 上色后的 alpha 上,对超过阈值的像素做连通域分割。这样识别出的每一片热区,边界与合计都严格对应用户屏幕上看到的那一块热斑,标注即所见。
这也解释了为什么识别必须承接第三、四篇的覆写:如果热力层没有通过 postProcesses 注入完成 gradient 上色,帧缓冲里就只有 splat 累加的原始 alpha,没有「与屏幕一致」的上色后语义;如果退回内置 Heatmap + 全量 Vector 源,又承载不了三百万级格网的瓦片化规模;若缺少第四篇的 active LOD composite 过滤,缩放跨档时屏上仍可能残留旧档位热力,识别也会与所见不一致。本篇的识别管线建立在同一个 WebGL 热力层之上,而不是另起一套几何聚类。
| 对比维度 | 方案 A:格网几何聚类 | 方案 B:FBO readback + 像素掩膜(本方案) |
|---|---|---|
| 热区定义 | 格心坐标的 GIS 邻接(距离 / 密度阈值) | 上色后 alpha 超过热区阈值的连通像素 |
| 与视觉一致性 | 聚类参数与 splat 半径、权重是两套体系,边界常与看到的热斑不符 | 掩膜与用户所见热斑同源,标注即所见 |
| 调参联动 | 调 splat 半径不影响聚类结果,需另调聚类参数 | 半径、模糊、透明度变化直接改变晕染,识别结果同步变化 |
| 合计归属 | 聚类簇内格网计数之和 | 连通域标签下格网计数之和,归属更贴合视觉边界 |
改造点:把识别对象从「数据」换成「画面」。原因:热区本来就是给用户看的,让识别与渲染共用同一套像素事实,比维护两套分立模型更可靠。
2. 承接第三、四篇覆写后的热区能力
热区识别不是 OpenLayers 的开箱能力,而是第三、四篇覆写链路在 CPU 侧的延伸。下表逐项说明「覆写 → 能力」的承接关系:
| 第三、四篇覆写 / 扩展 | 本篇由此得到的能力 | 说明(概念) |
|---|---|---|
postProcesses + gradient 上色 |
视觉一致的热区阈值 | 热区阈值作用在 postProcess 之后的 alpha 上,CCL 掩膜与用户所见热斑同源 |
| renderer active LOD 过滤 | 缩放跨档时标注与屏上热力一致 | 仅 composite 当前档瓦片,避免旧档位残留被误识别为热区 |
| splat + 瓦片级 GPU 融合 | 连通域边界贴合晕染 | 热区轮廓来自像素域 8-连通,而非格网 Voronoi 或多边形并集 |
MVT + AsShaders 权重 |
热区合计工单数 | 连通域内按标签聚合当前帧格网的格内工单数,与 splat 权重同源 |
| 同一 WebGL 热力层实例 | 缩放 / 筛选后标注与热力同步 | 平移缩放结束、瓦片到达触发重绘后重新识别;筛选切换先失效再刷新,时序与瓦片语义一致 |
| 掩膜缓存 + cpuOnly 路径 | 仅改阈值时快速重算 | 热区阈值调参可只重跑 CCL,不必重拉瓦片(依赖已捕获的 alpha 掩膜) |
第三篇解决「瓦片化热力怎么画」,第四篇解决「多 LOD 下屏上只显示当前档」,本篇解决「画出来的热斑怎么标、怎么点」,三者共享同一个覆写后的 WebGL 层。如果只做格网中心点聚类而不走 postProcess alpha,识别结果就会与 splat 视觉脱节——用户看到的亮斑和系统认定的热区对不上。这是本系列坚持 FBO 掩膜路径的产品原因,而不是 OL API 演示。
3. 识别管线
从地图交互到热区统计结果,完整管线如下:
逐段说明。
3.1 触发与防抖(trigger → render)
平移缩放结束(moveend)与瓦片加载完成(tileloadend)都会调度一次识别,约 300ms 防抖,避免连续交互期间反复读回。调度成功时先标记「待读回」,并通知 WebGL 层重绘一帧。
3.2 读回(render → readback)
读回必须发生在图层的 postrender 之后——只有这一帧的 splat 加性混合与 gradient 上色都执行完,帧缓冲里的 alpha 才与屏幕所见一致。实现上是从渲染助手把 postProcess 之后的全屏 RGBA 读回 CPU 侧,并做 Y 轴翻转对齐屏幕坐标系(WebGL 原点在左下)。这是对 OL 私有渲染结构的又一个使用点,与第三、四篇的注入点一样锁定 10.6.1,此处不展开细节。
3.3 掩膜(readback → mask)
对全屏 RGBA 直接做连通域太贵,先下采样成长边不超过 512 的掩膜栅格,每个栅格单元取其覆盖像素块内最大的 splat alpha——宁可保留小热斑,也不让平均化把细窄的热连通冲淡。postProcess 输出是预乘 alpha,且整体透明度也乘在 alpha 通道上,因此读回的 finalAlpha 要先按整体透明度反推回 splat 累积 alpha,再按热区阈值二值化,得到布尔热像素栅格。为何选 512、下采样如何与后续 MVT 归属对齐,见下文 §3.3.1。
3.3.1 为何下采样到长边 512 的掩膜
帧缓冲读回本身已是全视口 RGBA 的 CPU 拷贝;若连通域标注(CCL)在全分辨率上跑,主线程成本随视口面积线性增长——实测地图视口 CSS 如 1920×900 约 170 万像素(CSS 像素空间),而长边压到 512 后通常只有十余万像素(如 512×241),BFS 扫描与队列规模随之封顶。实现里用常量 HEATMAP_MASK_RASTER_MAX_SIZE = 512 表达的就是这层意图:控制 readback 之后的连通域分析开销。
下采样不是简单「缩小画面」,而是有意的保真策略:每个粗栅格单元覆盖 pixelScale × pixelScale 个 CSS 像素,单元内取覆盖块 splat alpha 的最大值(非平均)。这样粗化时宁可保留小热斑,也不让平均化把细窄的热连通冲淡,与 splat 晕染「亮处连通」的语义一致。
第三层动机是与格网归属查表共用同一套粗栅格:MVT 格心投屏后,用 floor(screenPx / pixelScale) 落到同一掩膜格上查连通域标签。识别、CCL、统计在同一 pixelScale 语义下完成,不会出现「全分辨率 CCL、粗栅格查表」两套尺度打架。
| 维度 | 做法 | 效果 |
|---|---|---|
| 性能 | 长边上限 512,CCL 在粗栅格上 BFS | 视口再大,连通域规模有顶;与每帧 FBO 读回成本解耦 |
| 保真 | 粗格内取 splat alpha 最大值 | 细窄热连通不易被平均化抹掉 |
| 一致 | MVT 查表与掩膜共用 pixelScale |
热区标签与格网合计在同一粗栅格上对齐 |
粗化会引入边界误差(§3.4.1 末尾再述)。在本方案验证视口下,长边 512 实测热区轮廓与合计可接受,属于性能与精度的工程折中,而非理论最优分辨率;若调大或调小该常量,需回归 CCL 耗时与边界误差。
3.4 连通域标注(mask → ccl)
这一步是经典的 CCL(Connected Component Labeling,连通分量标记)问题:输入是阈值化后的二值掩膜,输出是一张同尺寸的 label 图——背景为 0,每片连通区域得到一个递增的整数标签(1, 2, 3…)。连通性取 8-连通(上下左右加四个对角),而不是 4-连通:高斯晕染斜向相连是常态,8-连通更贴合肉眼对「连成片」的判断。算法概念与实现思路可参考炸鸡人博客《二值图像的连通域标记》(Seed-Filling 与 Two-Pass 讲解)与火山引擎开发者社区《连通域的原理与 Python 实现》(4/8 邻接概念)。本方案在浏览器侧用 BFS 实现,等价于 Seed-Filling 的 8 邻域蔓延:扫描到一个未标记的热像素即入队,逐层向外蔓延直到整片区域打满同一标签。掩膜规模由 §3.3.1 封顶,一次全图 BFS 的成本可控,不需要引入 OpenCV、scipy 这类图像库。
// CCL 概念伪代码:8-连通 BFS 种子填充
for (const pixel of raster) {
if (!isHot(pixel) || labeled(pixel)) continue
labelId += 1
queue.push(pixel)
while (queue.length) {
const p = queue.pop()
label[p] = labelId
for (const n of neighbors8(p)) // 8 邻域
if (isHot(n) && !labeled(n)) queue.push(n)
}
}
// 输出:label 图(0 = 背景,1..N = 连通域标签)
3.4.1 格网归属与坐标映射
CCL 产出的是掩膜上的连通域标签;要把 MVT 格网点的格内工单数归到某一片热区,需要把「格心在哪里」与「该像素属于哪个标签」对齐。热区归属不是格网中心的 GIS 邻接,而是两条数据线在同一套 CSS 屏幕像素 空间里汇合后的查表结果。
线 1(GPU 热力):postProcess 之后的帧缓冲读回 → 下采样为 ≤512 长边的 splat alpha 粗栅格 → 8-连通 CCL → 每个粗格一个连通域标签。
线 2(MVT 格网):从当前帧有效瓦片取出格心地理坐标 → getPixelFromCoordinate 投到 CSS 屏幕像素 → floor(screenPx / pixelScale) 落到掩膜格 → 查该格的连通域标签 → 同标签下累加格内工单数。
三套坐标系
实现里实际存在三套坐标,没有第四套「canvas 设备像素」参与 MVT↔掩膜查表——设备像素只在 FBO 读回时用 pixelRatio 还原成 CSS 尺寸,再下采样到掩膜格。
| 坐标系 | 含义 | 典型量级 | 角色 |
|---|---|---|---|
| 地理坐标 | 格心经纬度 | 度 | 格网属性、加权质心、ol/Overlay 锚点 |
| OL CSS 屏幕像素 | 与 map.getSize() 同系,视口左上为原点 |
约 1920×901 | MVT 投屏;掩膜 originPx、pixelScale 的单位 |
| 掩膜栅格索引 | 下采样后 (gx, gy) 或线性下标 |
长边 ≤512,如 512×241 | CCL 标签图、归属查表 |
DevTools 里常见的 WebGL canvas 形如 width="2880" height="1351"、transform: scale(0.666667):那是设备像素与 CSS 显示的叠加,与 getPixelFromCoordinate 返回的 CSS 像素不是同一尺度。OL 内部已用 pixelRatio 对齐,业务侧查表只认 CSS 视口。
数字示例:设备像素、CSS 视口与 512 掩膜
屏 A(canvas width=2880 height=1351,transform: matrix(0.666667, …)):
| 量 | 计算 | 值 |
|---|---|---|
| 设备像素 | canvas width / height | 2880 × 1351 |
pixelRatio |
2880 ÷ 1920 | 1.5 |
| CSS 视口 | 设备像素 ÷ pixelRatio | 1920 × 900.67 |
pixelScale |
max(cssWidth, cssHeight) ÷ 512 | 3.75 CSS px/格 |
| 掩膜尺寸 | ceil 除法 | 512 × 241 |
屏 B(canvas width=2561 height=1172,同样 scale(0.666667)):CSS 视口约 1707 × 781,pixelScale ≈ 3.33,掩膜约 512 × 235。
512 限制的是 CSS 逻辑长边,不是 canvas 的 2880 设备像素:FBO 先按设备分辨率读回,再除以 pixelRatio 得到 CSS 尺寸,最后按 pixelScale 合并到长边不超过 512 的粗格(块内取 splat alpha 最大值)。
映射 walkthrough(屏 A):某格网格心投屏得 screenPx = (750, 420),pixelScale = 3.75:
gx = floor(750 / 3.75) = 200
gy = floor(420 / 3.75) = 112
idx = 112 × 512 + 200 = 57544
连通域标签 = labelRaster[57544]
若标签为 0(非热区)或越界,该格网不参与任何热区统计;若标签为 3,其格内工单数累加到热区 3,质心按格内工单数对地理坐标加权。
正向映射(概念)
地理坐标 → getPixelFromCoordinate → screenPx(CSS 全分辨率)
screenPx → floor((screenPx - originPx) / pixelScale) → (gx, gy)
连通域标签 = labelRaster[gy × width + gx]
代码里没有把 MVT 点存成独立的「512 掩膜坐标」:始终保留全分辨率 screenPx,查表时即时除 pixelScale 取整;originPx 固定与视口左上角 [0, 0] 对齐。
坐标系关系(示意)
┌─────────────────────────────────────────────────────────────┐
│ OL Map 视口 (CSS px) 1920 × 901 │
│ origin (0,0) ─────────────────────────────► x │
│ │ │
│ │ MVT 格网: getPixelFromCoordinate → screenPx (CSS) │
│ │ 例: (750, 420) │
│ │ │ │
│ │ ▼ floor(/ pixelScale=3.75) │
│ │ 掩膜格 (gx=200, gy=112) ← 512×241 粗栅格 │
│ │ │ │
│ │ ▼ labelRaster[idx] → 连通域标签=3 │
│ ▼ │
│ y │
└─────────────────────────────────────────────────────────────┘
▲ 叠加关系
│
┌────────┴────────────────────────────────────────────────────┐
│ WebGL Canvas 设备像素 2880×1351 │
│ CSS 显示: transform scale(0.666667) → 视觉上 1920×901 │
│ FBO readPixels → 按 pixelRatio 还原 css 尺寸 → 下采样 512 │
└─────────────────────────────────────────────────────────────┘
反向显示:圆标与边界的两条路径
聚合完成后,不会把结果再存回掩膜坐标去画圆标。显示分两条路,锚点始终是地理坐标(或捕获时刻固化的地理四角),由 OL 每帧投屏。
| 路径 | 捕获 / 聚合 | 显示 | 是否经掩膜格回写屏幕 |
|---|---|---|---|
| 圆标 | 同连通域标签下累加格内工单数 + 地理加权质心 | ol/Overlay 锚定地理坐标 |
否;OL 每帧地理→屏幕 |
| 边界 | 捕获时掩膜格四角 CSS→地理 quad | ImageCanvas 每帧地理→canvas 像素 |
否;捕获一次,随视口重绘 |
圆标质心来自 MVT 格内工单数对地理坐标的加权平均,不是 CCL 像素质心,也不是掩膜格中心反投。仅当某热区没有任何格网计数可累加时,才 fallback 到掩膜格中心 CSS→地理——正常有数据时不走这条。
完整映射链(示意)
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 1:FBO → 掩膜 + CCL(仅分析用) │
│ 设备像素 FBO ──÷pixelRatio──► css 视口(全分辨率) │
│ css 视口 ──下采样(pixelScale)──► 掩膜格 (≤512×H) │
│ 掩膜格 + splatAlpha ──8-CCL──► labelRaster = 连通域标签 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 2:MVT 归属(查表) │
│ MVT 地理坐标 ──getPixelFromCoordinate──► screenPx (CSS) │
│ screenPx ──floor(/pixelScale)──► (gx,gy) ∈ 掩膜格 │
│ 连通域标签 = labelRaster[gy×W+gx] │
│ 同标签的 MVT:累加格内工单数,加权累加地理坐标 │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────┐ ┌────────────────────────────────┐
│ 显示:数字圆标 │ │ 显示:边界线 │
│ 加权质心(地理坐标) │ │ 捕获:掩膜格角点→CSS→地理 quad │
│ Overlay(position=地理) │ │ 绘制:地理→canvasPx(extent) │
│ OL 自动地理→屏幕 │ │ 不持久保存掩膜索引 │
└──────────────────────────┘ └────────────────────────────────┘
映射上的注意点
- 粗化误差:
pixelScale > 1时,同一掩膜格内多个 CSS 像素共享一个连通域标签;边界附近的格网点可能因粗化落在相邻热区格或冷区(标签 0)而被丢弃。 - 圆标位置与 CCL 形状解耦:圆标在 MVT 地理加权质心,热区轮廓来自 GPU alpha 的 CCL;两者通过
screenPx → 掩膜格 → 标签关联,质心不必落在热区视觉中心。 - 单点采样:归属查表只取格心落点那一格,不向邻域扩散;与 splat 晕染的软边可能略有偏差。
- 视口门控:
screenPx须在[0, mapSize)内;掩膜与 FBO 均对应当前视口。
3.5 采集(ccl → collect)
并行地,从当前帧的有效瓦片上采集每个格网的格心屏幕位置与格内工单数;投屏与查表公式见 §3.4.1。这里的关键是「同一帧语义」:参与采集的瓦片必须与 WebGL 当前帧渲染保持同一 active LOD——URL 的 LAYER= 与 sourceZ 判定和第四篇 renderer 完全是同一套——并且筛选条件一致,再按格网标识去重。画面画哪档,统计就算哪档。采集实现的瓦片过滤细节不展开,只需记住它与 renderer composite 共用瓦片事实。
3.6 聚合(collect → aggregate)
把每个格网单元的 screenPx 映射到掩膜格并读取连通域标签(§3.4.1 正向映射):同一标签下累加格内工单数得到热区合计;质心按格内工单数对地理坐标加权——工单更密的一侧会把圆标锚点拉向它,比几何中心更符合「热区重心」的直觉。没有热像素、或采集不到任何格网计数的标签直接丢弃(例如热斑恰好落在当前帧有效瓦片覆盖之外),最后按合计降序输出热区列表。圆标与边界的显示回地图路径见 §3.4.1「反向显示」。
3.7 性能分级
管线有两条成本不同的路径,由变更类型决定:
只调热区阈值时,掩膜栅格本身不变,走 cpuOnly 捷径:复用缓存的 splatAlpha 栅格,仅重跑阈值化、CCL 与聚合,不读帧缓冲、不重拉瓦片,两条路径使用各自独立的防抖定时器,互不取消。缩放换档或瓦片更新时画面内容已变,必须走全量读回重建掩膜。
4. 绘制
圆标与边界的坐标来源(地理加权质心、掩膜格捕获 quad)见 §3.4.1;本节说明两类可视化产物如何落到地图上。
识别结果有两类可视化产物:
| 能力 | 实现 | 为何 |
|---|---|---|
| 热区工单数圆标 | ol/Overlay DOM,质心锚点 |
不随缩放变形;可点击下钻;DOM 便于做分档视觉与层叠控制 |
| 热区边界 | ol/source/ImageCanvas + Image 图层 |
像素级贴合晕染边界;随视口缓存与重绘(见 §5) |
圆标。每个热区一个 DOM 圆标,锚定在加权质心上,中心数字为热区合计。视觉按合计的十进制位数分档:位数越多,圆越大、字号越大;颜色也随档位采样自同一条热力色带——合计只有个位、两位的小热区落在偏冷色段,位数越多越靠近热端。圆标因此与热斑视觉同源:大数字必然出现在更「红」的热区上,认知成本最低。选中某个热区时用加深底色的高亮圆标替换普通圆标。
边界。把连通域标签大于 0 的栅格单元轮廓描成阶梯形白色边线,叠加在热力层之上,勾勒出每片热区的外轮廓。为什么这条边界不用矢量面而要用 ImageCanvasSource,值得单独一节用 OL 源码说明。
5. 为什么用 ImageCanvasSource 绘制热区边界
OL 源码行为。ol/source/ImageCanvas.js 要求传入一个 canvasFunction(extent, resolution, pixelRatio, size, projection):每当图层需要影像时,OL 按当前视口(经 ratio 放大)调用它,由它返回一张画好的 canvas。返回结果被 source 缓存——revision 未变、resolution 与 pixelRatio 相同、且缓存 canvas 的 extent 包含本次请求 extent 时直接复用;任何一个条件不满足就重新回调 canvasFunction。若 canvas 内容本身变了(例如重新识别出新的标签栅格),则要显式调用 changed() 使缓存失效。interpolate 控制影像重采样是否线性插值,默认开启。
本方案缺口。热区边界是像素掩膜的阶梯形轮廓,不是预定义的矢量多边形。splat 晕染的边界天然在像素级:若把掩膜轮廓矢量化成 Polygon,要么轮廓追踪加顶点简化导致边界失真,要么保留全部折点导致几何动辄数千顶点、每次识别都要重建,成本与收益完全不对等。
选型。捕获时刻(CCL 完成时)把标签大于 0 的栅格单元四角通过 getCoordinateFromPixel 转成地理坐标,得到一批地理锚定的四边形;canvasFunction 内再把这些四边形映射回当前 canvas 的像素坐标,只描「与上下左右邻居标签不同」的外边,连成阶梯形轮廓:
// canvasFunction 概念伪代码:按当前视口重绘地理锚定的阶梯边界
function canvasFunction(extent, resolution, pixelRatio, size, projection) {
const canvas = createCanvas(size)
for (const cell of geoCells) { // 捕获时刻生成的地理锚定栅格
const quadPx = cell.quad.map(toCanvasPx) // 地理坐标 → 当前 canvas 像素
strokeOuterEdges(quadPx, cell.labelId) // 仅描与邻居标签不同的边
}
return canvas
}
因为四边形是地理锚定的,地图平移、缩放时 OL 会按新的 extent 与 resolution 重新回调 canvasFunction,同一批栅格被重新投影绘制,边界自然与热力跟手;同时把 interpolate 关掉,让重采样保持最近邻,维持像素级的阶梯感而不是被插值糊成软边。
| 对比维度 | 方案 A:Vector 多边形 | 方案 B:ImageCanvas(本方案) |
|---|---|---|
| 边界贴合度 | 需把像素掩膜矢量化(轮廓追踪 + 简化),易失真 | 像素级阶梯边界,与晕染天然一致 |
| 更新成本 | 每次识别重建矢量几何,顶点多时昂贵 | 重绘 canvas 描边,成本随栅格规模线性 |
| 视口跟随 | 矢量地理锚定,跟随免费 | 需维护捕获时刻的地理锚定;OL 缓存机制按 extent 自动重绘 |
| 交互能力 | 可做要素级选中、编辑 | 纯展示,命中交互交给 Overlay 圆标 |
热区边界是纯展示要素,不需要矢量交互;用 ImageCanvas 以最小的结构换来像素级贴合与免费的重绘调度,是这个场景下的合理选择。
6. 其他难点
active LOD 三方对齐。参与统计的瓦片、renderer 实际 composite 的瓦片、tileUrlFunction 请求的瓦片,必须使用同一套过滤语义——URL 的 LAYER= 参数、当前 sourceZ、当前筛选条件,三者任一脱节,统计结果就会与屏幕所见的热力不是同一批数据。这与第三篇「每帧重绘有效瓦片集」、第四篇「renderer 只画当前档」同源:统计不能自己另搞一套瓦片视图,必须跟随渲染使用的同一瓦片事实。
筛选切换后标注短暂归零。筛选条件变化时,管线先主动失效:清空圆标、边界与统计数字,避免旧标签指向新数据;随后瓦片源刷新,新瓦片到达并完成渲染后再重新识别,标注恢复。这段「先归零再恢复」是预期时序而非缺陷——与其展示与新筛选不匹配的旧热区,不如短暂清空。还有一个容易漏掉的兜底:新瓦片全部命中缓存时不会触发瓦片加载事件,需要再以地图渲染完成(rendercomplete)补一次调度,否则标注会一直停在归零态。
7. 热区点击查询:回扣数据层
热区圆标不只是展示,还是下钻入口。点击某个圆标时,聚合阶段已经收集好该热区归属的全部格网标识,直接发起一次 WFS 查询:
两个设计要点。
第一,查询不重复携带筛选维度。与第二篇的契约一致:热区点击 WFS 只按 关联格网标识 IN (...) 查询,不再附带事项专题、业务场景、发生年份、所属区县的 ECQL——当前这片热区的几何本身就是筛选生效后的产物,筛选已经在 MVT 层体现过了,回带四维度既冗余又容易因口径不一致查空。数据量上先以一次小分页请求探出总数,再一次拉齐,避免盲拉超大结果集。
第二,这条路径为什么成立,要回到第一篇。工单明细物化视图物化了「关联格网标识」列并为其建了索引,正是为这条 IN 反查准备的;格网标识在格网聚合物化视图刷新后保持稳定,热区合计来自 MVT 的格内工单数,明细来自 WFS——两种数据各走最优通道,又靠同一个稳定标识对齐。至此,总览篇的端到端数据流完整闭环:源业务表 → 双物化视图 → MVT 热力 + WFS 明细 → WebGL 渲染 → FBO 连通域热区 → 点击回到明细。
小结
- 热区是屏幕像素级的视觉对象:识别走「读回帧缓冲 → 下采样掩膜 → 8-连通域标注 → 聚合合计与质心」,标注即所见,这是它相对格网几何聚类的核心价值。
- 掩膜长边 512 封顶 CCL 开销,下采样取块内 splat alpha 最大值;MVT 归属与 CCL 共用
pixelScale查表,圆标与边界最终以地理坐标锚定,由 OL 或 ImageCanvas 投回屏幕(§3.3.1、§3.4.1)。 - 全部能力承接第三、四篇的覆写:没有 postProcess 上色后的 alpha,就没有视觉一致的热区阈值;识别与渲染共享同一个 WebGL 热力层,且计数采集与 renderer 共用 active LOD 语义。
- 热区边界用
ImageCanvasSource绘制地理锚定的阶梯轮廓,随视口缓存与重绘;圆标用ol/Overlay锚在加权质心,按合计位数分档视觉。 - 统计必须与渲染对齐同一 active LOD、同一筛选条件;筛选切换后的「先归零再恢复」是预期时序。
- 点击下钻只携带归属格网标识集合,依赖数据层双物化视图设计——「百万级点数据 → 热力 → 热区 → 明细」的链路至此闭环。
系列导航
- 总览:百万级点数据:MVT + WebGL 热力渲染与 FBO 连通域热区工单统计
- 01:数据层:双物化视图与三格网 LOD
- 02:服务层:MVT 瓦片与按需明细查询
- 03:前端:热力图 WebGL 渲染管线
- 04:前端:矢量瓦片 Renderer 覆写与 LOD composite
- 05:前端:热区识别、计算与绘制
浙公网安备 33010602011771号