专题的多圆显示区:前台按并集裁剪迷雾

上一批把显示区改成了「一组圆」,这一批让前台真正认多个圆。

- clip-path 从 circle() 换成 path():circle() 只表达得了一个圆。每个圆一段
  圆弧子路径、**同绕向**(sweep=0),nonzero 填充规则下多段自然取并集,
  重叠处不会互相挖洞。
- 拼路径的活单独放 fog-clip.js(rond-map.js 是个大 IIFE,函数取不出来),
  这样能直接跑 node 单测;layout 里必须排在 rond-map.js 之前加载。
- 顺带清掉已经死掉的 fitCircle:多圆之后「圆心 + 半径」表达不了,留着就是
  两套并行的贴合口径。fitParams 现在只给 fitBBox(单圆时外接矩形正是那个
  圆的外接正方形,视野等价)。
- 守卫改名 TestTopicFogIsClippedToAreas,钉住两端(模板发 fogAreas ↔ 脚本认
  fogAreas)与两处顺序(裁剪注册在 applyFit 之后、fog-clip.js 在 rond-map.js
  之前);fogAreas 必须**无条件**下发(空数组 = 不限区域),不要包在条件里。
- verify.sh 挂进前端单测:以前只靠人记得手动跑 = 等于没有。找不到 node 直接
  失败而不是跳过(跳过会让验收看着全绿、实际一行前端脚本都没验)。

验证(临时两圆专题,SQL 造的,已删):
- fog-clip.test.js 15 条:把生成的 path 解析回圆弧再验几何,含绕向一致性与
  半径 0/负数的夹取。
- 端到端像素验证:自校准出「CSS 容器坐标 → 图像像素」(scale≈2、offset≈19.2,
  正是 site_cdp 的 --clip 外扩 10px×scale,别再手算);三张掩码图证明并集语义
  —— A 独有点只有 A 亮、B 独有点只有 B 亮、**重叠中心两圆都亮且并集也亮
  (无洞)**,整体集合差异 0.013%。
- 迷雾掩码:86,776 个像素,按两圆并集的解析式判 **圆外 0 个**;两个圆内分别
  有 52,609 / 46,757 个迷雾像素 ⇒ 第二个圆真的被认了,不是只看第一个。
This commit is contained in:
tamakyi
2026-10-12 16:44:55 +08:00
parent 1bb35dbbc9
commit 44763dfc3f
10 changed files with 277 additions and 59 deletions
+30 -17
View File
@@ -704,24 +704,42 @@
// 专题可以在管理页关掉迷雾(opts.showFog=false),此时图层、浓度滑杆、图层开关都不出现。
var wantFog = opts.showFog !== false;
var fogLayer = null;
/* 专题可以圈一个区域(opts.fogArea = {lat, lon, radiusKm}):此时迷雾只在该圆内显示。
做法是给迷雾**单独一个 pane** 再对这个 pane 做圆形 clip-path —— 不能直接裁默认的
/* 专题可以圈一组区域(opts.fogAreas = [{lat, lon, radiusKm}, ...]):此时迷雾只在
这些圆的**并集**内显示。
做法是给迷雾**单独一个 pane** 再对这个 pane 做 clip-path —— 不能直接裁默认的
tilePane,底图也在那里面,会把地图一起裁掉。
圆心必须跟 fitCircle 一样先 project() 到底图坐标系,否则会整体偏出几百米。
多圆用 path() 而不是 circle()(后者只表达得了一个圆);拼路径的活交给 fog-clip.js
(那样能跑 node 单测),这里只负责把经纬度换算成容器像素。
圆心必须像 fitBBox 那样先 project() 到底图坐标系,否则会整体偏出几百米。
clip-path 不认识的老浏览器只是「不裁剪」(退回全局迷雾),不会出错。 */
var fogPane = null, applyFogClip = null;
if (wantFog && cfg.fog && cfg.fog.blocks > 0 && opts.fogArea && opts.fogArea.radiusKm > 0) {
var fogPane = null, applyFogClip = null, fogCircles = [];
var fogAreaOpts = opts.fogAreas || [];
for (var fa = 0; fa < fogAreaOpts.length; fa++) {
var area = fogAreaOpts[fa];
if (!area || !(area.radiusKm > 0)) continue;
var areaC = project(area.lat, area.lon, def.crs);
fogCircles.push({
center: L.latLng(areaC[0], areaC[1]),
// 正北方向同经度偏移 radiusKm,用来量出容器像素半径(随缩放自然变化)
north: L.latLng(areaC[0] + (area.radiusKm * 1000) / 111320, areaC[1])
});
}
if (wantFog && cfg.fog && cfg.fog.blocks > 0 && fogCircles.length) {
map.createPane('rondFog');
fogPane = map.getPane('rondFog');
fogPane.style.zIndex = 250; // 底图瓦片 200 之上、覆盖物 400 之下
fogPane.style.pointerEvents = 'none';
var fogC = project(opts.fogArea.lat, opts.fogArea.lon, def.crs);
var fogCenter = L.latLng(fogC[0], fogC[1]);
var fogNorth = L.latLng(fogCenter.lat + (opts.fogArea.radiusKm * 1000) / 111320, fogCenter.lng);
applyFogClip = function () {
var p = map.latLngToContainerPoint(fogCenter);
var r = Math.max(1, p.distanceTo(map.latLngToContainerPoint(fogNorth)));
var v = 'circle(' + r.toFixed(1) + 'px at ' + p.x.toFixed(1) + 'px ' + p.y.toFixed(1) + 'px)';
// 脚本没加载上就退回「不裁剪」,别抛错把地图初始化打断
if (!window.rondFogClip) return;
var px = [];
for (var i = 0; i < fogCircles.length; i++) {
var p = map.latLngToContainerPoint(fogCircles[i].center);
px.push({ x: p.x, y: p.y,
r: Math.max(1, p.distanceTo(map.latLngToContainerPoint(fogCircles[i].north))) });
}
var v = window.rondFogClip.pathFor(px);
if (!v) return;
fogPane.style.clipPath = v;
fogPane.style.webkitClipPath = v;
};
@@ -764,13 +782,8 @@
// 专题页要的是「区域视野」而不是「点集视野」:只框住点的话,
// 只去过一两个地方时视野会缩得过小,区域内的迷雾也看不全。
// 传 fitCircle(中心 + 半径)或 fitBBox 就能覆盖默认的点集贴合。
// 传 fitBBox(专题显示区的并集外接矩形)就能覆盖默认的点集贴合。
var applyFit = function () {
if (opts.fitCircle && opts.fitCircle.radiusKm > 0) {
var c = project(opts.fitCircle.lat, opts.fitCircle.lon, def.crs);
map.fitBounds(L.latLng(c[0], c[1]).toBounds(opts.fitCircle.radiusKm * 2000));
return;
}
if (opts.fitBBox) {
var b = opts.fitBBox;
var p1 = project(b.minLat, b.minLon, def.crs), p2 = project(b.maxLat, b.maxLon, def.crs);