|
|
@@ -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();
|
|
|
},
|
|
|
|
|
|
/**
|