浏览代码

将子区蒙层从 AMap.Circle 改为凸包多边形,更贴合实际路口分布

  - 用 Graham Scan 凸包算法替代圆形,避免离群路口撑爆半径覆盖全图
  - 凸包向外扩展 10% 留出视觉间距,半透明填充 + 虚线描边
画安 1 周之前
父节点
当前提交
51965cbf74
共有 1 个文件被更改,包括 54 次插入21 次删除
  1. 54 21
      src/components/TongzhouTrafficMap.vue

+ 54 - 21
src/components/TongzhouTrafficMap.vue

@@ -1362,7 +1362,7 @@ export default {
     },
 
     /**
-     * 对外暴露:为指定子区路口列表绘制形蒙层,点击不同子区时自动替换上一个
+     * 对外暴露:为指定子区路口列表绘制凸包多边形蒙层,点击不同子区时自动替换上一个
      * @param {Array<{lng: number, lat: number}>} leaves - 子区内所有叶子路口节点
      */
     drawSubAreaCircle(leaves) {
@@ -1370,25 +1370,22 @@ export default {
 
       this.clearSubAreaOverlays();
 
-      // 计算圆心(路口坐标均值)
-      const cx = leaves.reduce((s, n) => s + Number(n.lng), 0) / leaves.length;
-      const cy = leaves.reduce((s, n) => s + Number(n.lat), 0) / leaves.length;
-
-      // 计算各路口到圆心的最大距离(米),cosLat 修正经度方向的比例
-      const cosLat = Math.cos(cy * Math.PI / 180);
-      let maxDist = 0;
-      for (const n of leaves) {
-        const dx = (Number(n.lng) - cx) * 111000 * cosLat;
-        const dy = (Number(n.lat) - cy) * 111000;
-        const d = Math.sqrt(dx * dx + dy * dy);
-        if (d > maxDist) maxDist = d;
-      }
-      // 半径 = 最大距离 × 1.3,最小保底 500m
-      const radius = Math.max(maxDist * 1.3, 500);
+      const pts = leaves.map(n => [Number(n.lng), Number(n.lat)]);
+      if (pts.length < 3) return;
+
+      const hull = this._convexHull(pts);
+      if (hull.length < 3) return;
+
+      // 以凸包重心为基准,向外扩展 10% 留出视觉间距
+      const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;
+      const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;
+      const paddedHull = hull.map(p => [
+        cx + (p[0] - cx) * 1.1,
+        cy + (p[1] - cy) * 1.1,
+      ]);
 
-      const circle = new this.AMap.Circle({
-        center: [cx, cy],
-        radius,
+      const polygon = new this.AMap.Polygon({
+        path: paddedHull,
         fillColor: '#4A9EFF',
         fillOpacity: 0.15,
         strokeColor: '#4A9EFF',
@@ -1400,8 +1397,44 @@ export default {
         bubble: true,
       });
 
-      this.subAreaOverlays.push(circle);
-      this.map.add(circle);
+      this.subAreaOverlays.push(polygon);
+      this.map.add(polygon);
+    },
+
+    /**
+     * Graham Scan 凸包算法
+     * @param {Array<[number, number]>} points - 经纬度点数组
+     * @returns {Array<[number, number]>} 凸包顶点(逆时针)
+     */
+    _convexHull(points) {
+      if (points.length < 3) return [...points];
+
+      const p0 = points.reduce((best, p) =>
+        p[1] < best[1] || (p[1] === best[1] && p[0] < best[0]) ? p : best
+      );
+
+      const cross = (o, a, b) =>
+        (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
+
+      const dist2 = (a, b) =>
+        (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
+
+      const sorted = points
+        .filter(p => p !== p0)
+        .sort((a, b) => {
+          const c = cross(p0, a, b);
+          if (c !== 0) return c > 0 ? -1 : 1;
+          return dist2(p0, a) - dist2(p0, b);
+        });
+
+      const hull = [p0];
+      for (const p of sorted) {
+        while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) {
+          hull.pop();
+        }
+        hull.push(p);
+      }
+      return hull;
     },
 
     /**