Просмотр исходного кода

路口 marker 改为视口裁剪:首屏只挂当前视口内的 marker,拖图按 bbox 增量加载
- 引入单一调度入口 computeVisibleMarkers:按 map.getBounds()(外
扩 15% buffer)+ activeLegends 双重过滤,diff 当前挂载集后批量
add/remove
- moveend / zoomend → scheduleViewportRefresh(debounce 100ms)
- drawStaticRoutes 不再直接 map.add,覆盖物入组后委托调度器
- updateMapDisplay / toggleAll / toggleRouteVisible 全部统一走调度器
- 路线(干线协调/勤务路线)/ 起终点 marker 不参与裁剪,按 legend
状态全开/全关
- InfoWindow 打开期间该 marker 进保护集,避免被裁掉造成弹窗错位
- beforeDestroy 清理 debounce timer 与挂载集

画安 1 день назад
Родитель
Сommit
f12f21faea
1 измененных файлов с 180 добавлено и 28 удалено
  1. 180 28
      src/components/TongzhouTrafficMap.vue

+ 180 - 28
src/components/TongzhouTrafficMap.vue

@@ -94,6 +94,12 @@ export default {
       startEndMarkers: [], // 起点/终点 marker 引用,缩放时需重建
       subAreaOverlays: [], // 子区蒙层覆盖物
       _subAreaParams: null, // 子区绘制参数,缩放时重绘用
+      // ── 视口裁剪相关(详见 pyscripts/AMap路口marker视口加载与聚合设计.md Phase 1)──
+      mountedMarkerSet: new Set(),       // 当前已挂载到 map 的 overlay 引用
+      _viewportRefreshTimer: null,       // moveend/zoomend 合并 debounce 句柄
+      viewportBufferRatio: 0.15,         // 视口外扩比例,避免边缘 marker 闪烁
+      viewportRefreshDebounceMs: 100,    // 拖图停下后多久执行一次重算
+      _infoWindowProtectedMarker: null,  // InfoWindow 打开时的 marker,期间不参与裁剪
     };
   },
   mounted() {
@@ -123,6 +129,14 @@ export default {
     this.isComponentDestroyed = true;
     this.drawSeq += 1;
 
+    // 1.5 清理视口裁剪 debounce 句柄与挂载集
+    if (this._viewportRefreshTimer) {
+      clearTimeout(this._viewportRefreshTimer);
+      this._viewportRefreshTimer = null;
+    }
+    if (this.mountedMarkerSet) this.mountedMarkerSet.clear();
+    this._infoWindowProtectedMarker = null;
+
     // 2. 关闭弹窗
     if (this.infoWindow) {
       try {
@@ -222,6 +236,141 @@ export default {
     }
   },
   methods: {
+    // ==========================================================
+    // 视口裁剪(Phase 1,详见 pyscripts/AMap路口marker视口加载与聚合设计.md)
+    // ==========================================================
+
+    /**
+     * 单一调度入口:根据 map.getBounds() 与 activeLegends 计算应显示的 marker 集合,
+     * 与已挂载集合做 diff,批量 add/remove。
+     */
+    computeVisibleMarkers() {
+      if (!this.isMapReady()) return;
+      const bounds = this.map.getBounds();
+      if (!bounds) return;
+
+      // 视口外扩 buffer,避免边缘 marker 在拖动停下时立刻消失
+      const expanded = this.expandBounds(bounds, this.viewportBufferRatio);
+
+      const visibleLegendNames = new Set(this.activeLegends);
+      const shouldShow = new Set();
+
+      // 收集应显示的 overlay
+      Object.keys(this.routeGroups).forEach(name => {
+        const overlays = this.routeGroups[name];
+        if (!overlays || overlays.length === 0) return;
+        if (!visibleLegendNames.has(name)) return;
+
+        for (const o of overlays) {
+          // 路线(折线/弧线/装饰)—— 不参与视口裁剪
+          if (!this.isMarkerNode(o)) {
+            shouldShow.add(o);
+            continue;
+          }
+          // 起终点 marker —— 不参与视口裁剪(数量极少)
+          if (this.isStartEndMarker(o)) {
+            shouldShow.add(o);
+            continue;
+          }
+          // 普通路口 marker —— 按视口判定
+          const pos = this.getOverlayPosition(o);
+          if (pos && this.boundsContains(expanded, pos)) {
+            shouldShow.add(o);
+          }
+        }
+      });
+
+      // InfoWindow 打开期间 marker 豁免:始终保留以避免底层 marker 被移除导致弹窗错位
+      if (this._infoWindowProtectedMarker) {
+        shouldShow.add(this._infoWindowProtectedMarker);
+      }
+
+      // diff
+      const toAdd = [];
+      const toRemove = [];
+      shouldShow.forEach(o => {
+        if (!this.mountedMarkerSet.has(o)) toAdd.push(o);
+      });
+      this.mountedMarkerSet.forEach(o => {
+        if (!shouldShow.has(o)) toRemove.push(o);
+      });
+
+      if (toAdd.length) {
+        try { this.map.add(toAdd); } catch (e) { void e; }
+        toAdd.forEach(o => this.mountedMarkerSet.add(o));
+      }
+      if (toRemove.length) {
+        try { this.map.remove(toRemove); } catch (e) { void e; }
+        toRemove.forEach(o => this.mountedMarkerSet.delete(o));
+      }
+    },
+
+    /**
+     * 拖图/缩放后合并触发的视口刷新;100ms 内多次调用合并为一次。
+     */
+    scheduleViewportRefresh() {
+      if (this._viewportRefreshTimer) return;
+      this._viewportRefreshTimer = setTimeout(() => {
+        this._viewportRefreshTimer = null;
+        this.computeVisibleMarkers();
+      }, this.viewportRefreshDebounceMs);
+    },
+
+    /**
+     * 视口外扩 ratio 比例,避免在边界处 marker 闪入闪出。
+     */
+    expandBounds(bounds, ratio) {
+      const sw = bounds.getSouthWest();
+      const ne = bounds.getNorthEast();
+      const dLng = (ne.lng - sw.lng) * ratio;
+      const dLat = (ne.lat - sw.lat) * ratio;
+      return new this.AMap.Bounds(
+        [sw.lng - dLng, sw.lat - dLat],
+        [ne.lng + dLng, ne.lat + dLat]
+      );
+    },
+
+    /**
+     * 兼容多种 position 形态([lng,lat] 数组 / LngLat 对象)的 contains 判定。
+     */
+    boundsContains(bounds, pos) {
+      try {
+        if (Array.isArray(pos)) {
+          return bounds.contains(new this.AMap.LngLat(pos[0], pos[1]));
+        }
+        return bounds.contains(pos);
+      } catch (e) {
+        return false;
+      }
+    },
+
+    isMarkerNode(o) {
+      return this.AMap && o instanceof this.AMap.Marker;
+    },
+
+    isStartEndMarker(o) {
+      if (!o || !this.startEndMarkers || this.startEndMarkers.length === 0) return false;
+      for (const s of this.startEndMarkers) {
+        if (s.marker === o) return true;
+      }
+      return false;
+    },
+
+    /**
+     * 取 overlay 经纬位置:优先用 marker.getPosition(),回退到 extData.position。
+     */
+    getOverlayPosition(o) {
+      if (!o) return null;
+      if (typeof o.getPosition === 'function') {
+        const p = o.getPosition();
+        if (p) return p;
+      }
+      const ext = (typeof o.getExtData === 'function') ? o.getExtData() : null;
+      return ext && ext.position;
+    },
+
+    // ==========================================================
+
     /**
      * 动态加载地图数据
      * @returns {Promise<void>}
@@ -285,22 +434,13 @@ export default {
     },
 
     /**
-     * 更新地图显示状态
+     * 更新地图显示状态:统一委托给视口裁剪调度器,
+     * 由 computeVisibleMarkers 综合 activeLegends + bounds 决定 add/remove。
      */
     updateMapDisplay() {
       if (this.infoWindow) this.infoWindow.close();
       if (!this.isMapReady()) return;
-
-      Object.keys(this.routeGroups).forEach(name => {
-        const overlays = this.routeGroups[name];
-        if (overlays && overlays.length > 0) {
-          if (this.activeLegends.includes(name)) {
-            this.map.add(overlays);
-          } else {
-            this.map.remove(overlays);
-          }
-        }
-      });
+      this.computeVisibleMarkers();
     },
 
     /**
@@ -343,6 +483,15 @@ export default {
             this.rebuildStartEndMarkers();
             this.rebuildDotMarkers();
             this.rebuildSubAreaText();
+            // 视口裁剪:缩放后重算视口内 marker
+            this.scheduleViewportRefresh();
+          }
+        });
+
+        // 视口裁剪:拖图停下后重算视口内 marker(debounce 100ms)
+        this.map.on('moveend', () => {
+          if (!this.isComponentDestroyed) {
+            this.scheduleViewportRefresh();
           }
         });
       } catch (err) {
@@ -393,7 +542,7 @@ export default {
             })
           ).filter(Boolean);
           this.routeGroups[config.name] = markers;
-          if (this.activeLegends.includes(config.name)) this.map.add(markers);
+          // 视口裁剪:不直接 add,由 computeVisibleMarkers 统一处理
           continue;
         }
 
@@ -435,7 +584,8 @@ export default {
 
           if (overlays.length > 0) {
             this.routeGroups[config.name].push(...overlays);
-            if (this.activeLegends.includes(config.name)) this.map.add(overlays);
+            // 视口裁剪:每批新覆盖物入组后触发一次重算,让线/路口逐步出现在视口
+            this.computeVisibleMarkers();
           }
 
           await this.sleep(80);
@@ -445,6 +595,9 @@ export default {
           this.$emit('bindTrunkMenuTree', trunkSegments);
         }
       }
+
+      // 全部 routeGroups 准备好后再做一次终态刷新
+      this.computeVisibleMarkers();
     },
 
     /**
@@ -477,6 +630,8 @@ export default {
       this.routeGroups = {};
       this.startEndMarkers = [];
       this.dotMarkers = [];
+      // 重置视口裁剪挂载集(已 map.remove 过,这里只清引用)
+      this.mountedMarkerSet.clear();
     },
 
     /**
@@ -1104,6 +1259,8 @@ export default {
           const extData = e.target.getExtData();
           if (this.$route && this.$route.path === '/home') {
             this.cancelCloseInfoWindow();
+            // 视口裁剪:标记当前展开 InfoWindow 的 marker,期间豁免裁剪
+            this._infoWindowProtectedMarker = e.target;
             this.openLightInfo(extData, e.lnglat);
           }
           const pixel = this.map.lngLatToContainer(e.lnglat);
@@ -1114,6 +1271,7 @@ export default {
           if (this.isComponentDestroyed) return;
           if (this.$route && this.$route.path === '/home') {
             this.cancelCloseInfoWindow();
+            this._infoWindowProtectedMarker = e.target;
             this.openLightInfo(e.target.getExtData(), e.lnglat);
           }
           const pixel = this.map.lngLatToContainer(e.lnglat);
@@ -1190,6 +1348,10 @@ export default {
           offset: new this.AMap.Pixel(0, -20),
           autoMove: false
         });
+        // 视口裁剪:InfoWindow 关闭时解除 marker 保护
+        this.infoWindow.on('close', () => {
+          this._infoWindowProtectedMarker = null;
+        });
       }
 
       this.infoWindow.setContent(content);
@@ -1279,6 +1441,7 @@ export default {
 
     /**
      * 切换"所有图例可见项"的开关。常隐项(hideInLegend,如 降级)始终保留显示。
+     * 视口裁剪:只更新 activeLegends,实际 add/remove 由 computeVisibleMarkers 统一处理。
      */
     toggleAll() {
       if (!this.isMapReady()) return;
@@ -1289,40 +1452,29 @@ export default {
       const targetState = !this.isAllSelected;
 
       if (targetState) {
-        // 全选可见项 + 保留常隐项
         this.activeLegends = Array.from(new Set([...alwaysOnNames, ...visibleNames]));
-        visibleNames.forEach(name => {
-          const overlays = this.routeGroups[name];
-          if (overlays && overlays.length > 0) this.map.add(overlays);
-        });
       } else {
-        // 取消所有可见项,常隐项保留在 activeLegends,对应图层不动
         this.activeLegends = [...alwaysOnNames];
-        visibleNames.forEach(name => {
-          const overlays = this.routeGroups[name];
-          if (overlays && overlays.length > 0) this.map.remove(overlays);
-        });
         if (this.infoWindow) this.infoWindow.close();
       }
+      this.computeVisibleMarkers();
     },
 
     /**
-     * 切换指定路线的可见性
+     * 切换指定路线的可见性。视口裁剪:仅更新 activeLegends,由调度器决定 add/remove。
      * @param {string} name - 路线名称
      */
     toggleRouteVisible(name) {
       if (!this.isMapReady()) return;
 
-      const overlays = this.routeGroups[name] || [];
       const index = this.activeLegends.indexOf(name);
       if (index > -1) {
         this.activeLegends.splice(index, 1);
-        this.map.remove(overlays);
         if (this.infoWindow) this.infoWindow.close();
       } else {
         this.activeLegends.push(name);
-        this.map.add(overlays);
       }
+      this.computeVisibleMarkers();
     },
 
     /**