去掉全局 id="deck-canvas" 路径,改为实例级 canvasRef,修复可能出现的 Uncaught Error: deck.gl: assertion failed 的问题
This commit is contained in:
@@ -127,7 +127,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key
|
const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key
|
||||||
|
|
||||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
const deckLayerRef = useRef<DeckLayer | null>(null);
|
const deckLayerRef = useRef<DeckLayer | null>(null);
|
||||||
|
const isDisposingRef = useRef(false);
|
||||||
|
const pendingTimeoutsRef = useRef<number[]>([]);
|
||||||
|
|
||||||
const [map, setMap] = useState<OlMap>();
|
const [map, setMap] = useState<OlMap>();
|
||||||
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
||||||
@@ -518,14 +521,37 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
// The map and layer instances are intentionally rebuilt only when workspace or extent changes.
|
// The map and layer instances are intentionally rebuilt only when workspace or extent changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current) return;
|
if (!mapRef.current) return;
|
||||||
|
if (!canvasRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isDisposingRef.current = false;
|
||||||
|
|
||||||
|
const addTimeout = (callback: () => void, delay: number) => {
|
||||||
|
const timerId = window.setTimeout(() => {
|
||||||
|
pendingTimeoutsRef.current = pendingTimeoutsRef.current.filter(
|
||||||
|
(id) => id !== timerId,
|
||||||
|
);
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
|
callback();
|
||||||
|
}, delay);
|
||||||
|
pendingTimeoutsRef.current.push(timerId);
|
||||||
|
return timerId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPendingTimeouts = () => {
|
||||||
|
pendingTimeoutsRef.current.forEach((id) => clearTimeout(id));
|
||||||
|
pendingTimeoutsRef.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
// 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示
|
// 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示
|
||||||
junctionSource.on("tileloadend", (event) => {
|
const handleJunctionTileLoadEnd = (event: any) => {
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (event.tile instanceof VectorTile) {
|
if (event.tile instanceof VectorTile) {
|
||||||
const renderFeatures = event.tile.getFeatures();
|
const renderFeatures = event.tile.getFeatures();
|
||||||
const data = new Map();
|
const data = new Map();
|
||||||
|
|
||||||
renderFeatures.forEach((renderFeature) => {
|
renderFeatures.forEach((renderFeature: any) => {
|
||||||
const props = renderFeature.getProperties();
|
const props = renderFeature.getProperties();
|
||||||
const featureId = props.id;
|
const featureId = props.id;
|
||||||
if (featureId && !junctionDataIds.current.has(featureId)) {
|
if (featureId && !junctionDataIds.current.has(featureId)) {
|
||||||
@@ -554,14 +580,15 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Junction tile load error:", error);
|
console.error("Junction tile load error:", error);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
pipeSource.on("tileloadend", (event) => {
|
const handlePipeTileLoadEnd = (event: any) => {
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
try {
|
try {
|
||||||
if (event.tile instanceof VectorTile) {
|
if (event.tile instanceof VectorTile) {
|
||||||
const renderFeatures = event.tile.getFeatures();
|
const renderFeatures = event.tile.getFeatures();
|
||||||
const data = new Map();
|
const data = new Map();
|
||||||
|
|
||||||
renderFeatures.forEach((renderFeature) => {
|
renderFeatures.forEach((renderFeature: any) => {
|
||||||
try {
|
try {
|
||||||
const props = renderFeature.getProperties();
|
const props = renderFeature.getProperties();
|
||||||
const featureId = props.id;
|
const featureId = props.id;
|
||||||
@@ -634,7 +661,9 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Pipe tile load error:", error);
|
console.error("Pipe tile load error:", error);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
junctionSource.on("tileloadend", handleJunctionTileLoadEnd);
|
||||||
|
pipeSource.on("tileloadend", handlePipeTileLoadEnd);
|
||||||
// 监听 junctionsLayer 的 visible 变化
|
// 监听 junctionsLayer 的 visible 变化
|
||||||
const handleJunctionVisibilityChange = () => {
|
const handleJunctionVisibilityChange = () => {
|
||||||
const isVisible = junctionsLayer.getVisible();
|
const isVisible = junctionsLayer.getVisible();
|
||||||
@@ -748,6 +777,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
// 持久化视图(中心点 + 缩放),防抖写入 localStorage
|
// 持久化视图(中心点 + 缩放),防抖写入 localStorage
|
||||||
const persistView = debounce(() => {
|
const persistView = debounce(() => {
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
try {
|
try {
|
||||||
const view = map.getView();
|
const view = map.getView();
|
||||||
const center = view.getCenter();
|
const center = view.getCenter();
|
||||||
@@ -765,7 +795,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 监听缩放变化并持久化,同时更新 currentZoom
|
// 监听缩放变化并持久化,同时更新 currentZoom
|
||||||
const handleViewChange = () => {
|
const handleViewChange = () => {
|
||||||
setTimeout(() => {
|
addTimeout(() => {
|
||||||
const zoom = map.getView().getZoom() || 0;
|
const zoom = map.getView().getZoom() || 0;
|
||||||
setCurrentZoom(zoom);
|
setCurrentZoom(zoom);
|
||||||
persistView();
|
persistView();
|
||||||
@@ -774,7 +804,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
map.getView().on("change", handleViewChange);
|
map.getView().on("change", handleViewChange);
|
||||||
|
|
||||||
// 初始化当前缩放级别并强制触发瓦片加载
|
// 初始化当前缩放级别并强制触发瓦片加载
|
||||||
setTimeout(() => {
|
addTimeout(() => {
|
||||||
const initialZoom = map.getView().getZoom() || 11;
|
const initialZoom = map.getView().getZoom() || 11;
|
||||||
setCurrentZoom(initialZoom);
|
setCurrentZoom(initialZoom);
|
||||||
// 强制触发地图渲染,让瓦片加载事件触发
|
// 强制触发地图渲染,让瓦片加载事件触发
|
||||||
@@ -788,11 +818,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
latitude: 0,
|
latitude: 0,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
},
|
},
|
||||||
canvas: "deck-canvas",
|
canvas: canvasRef.current,
|
||||||
controller: false, // 由 OpenLayers 控制视图
|
controller: false, // 由 OpenLayers 控制视图
|
||||||
layers: [],
|
layers: [],
|
||||||
});
|
});
|
||||||
const deckLayer = new DeckLayer(deck, {
|
const deckLayer = new DeckLayer(deck, canvasRef.current, {
|
||||||
name: "deckLayer",
|
name: "deckLayer",
|
||||||
value: "deckLayer",
|
value: "deckLayer",
|
||||||
});
|
});
|
||||||
@@ -802,19 +832,37 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
return () => {
|
return () => {
|
||||||
|
isDisposingRef.current = true;
|
||||||
|
clearPendingTimeouts();
|
||||||
|
debouncedUpdateDataRef.current?.cancel();
|
||||||
|
persistView.cancel();
|
||||||
|
junctionSource.un("tileloadend", handleJunctionTileLoadEnd);
|
||||||
|
pipeSource.un("tileloadend", handlePipeTileLoadEnd);
|
||||||
|
map.getView().un("change", handleViewChange);
|
||||||
junctionsLayer.un("change:visible", handleJunctionVisibilityChange);
|
junctionsLayer.un("change:visible", handleJunctionVisibilityChange);
|
||||||
pipesLayer.un("change:visible", handlePipeVisibilityChange);
|
pipesLayer.un("change:visible", handlePipeVisibilityChange);
|
||||||
|
if (deckLayerRef.current && !deckLayerRef.current.isDisposedLayer()) {
|
||||||
|
try {
|
||||||
|
map.removeLayer(deckLayerRef.current);
|
||||||
|
} catch {
|
||||||
|
// Layer may have already been removed during teardown.
|
||||||
|
}
|
||||||
|
deckLayerRef.current.disposeDeck();
|
||||||
|
}
|
||||||
|
deckLayerRef.current = null;
|
||||||
|
setDeckLayer(undefined);
|
||||||
map.setTarget(undefined);
|
map.setTarget(undefined);
|
||||||
map.dispose();
|
map.dispose();
|
||||||
deck.finalize();
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [MAP_WORKSPACE, MAP_EXTENT]);
|
}, [MAP_WORKSPACE, MAP_EXTENT]);
|
||||||
|
|
||||||
// 当数据变化时,更新 deck.gl 图层
|
// 当数据变化时,更新 deck.gl 图层
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
const deckLayer = deckLayerRef.current;
|
const deckLayer = deckLayerRef.current;
|
||||||
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
||||||
|
if (deckLayer.isDisposedLayer()) return;
|
||||||
if (!mergedJunctionData.length) return;
|
if (!mergedJunctionData.length) return;
|
||||||
if (!mergedPipeData.length) return;
|
if (!mergedPipeData.length) return;
|
||||||
const junctionTextLayer = new TextLayer({
|
const junctionTextLayer = new TextLayer({
|
||||||
@@ -964,6 +1012,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 控制流动动画开关
|
// 控制流动动画开关
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isDisposingRef.current) return;
|
||||||
if (pipeText === "flow" && currentPipeCalData.length > 0) {
|
if (pipeText === "flow" && currentPipeCalData.length > 0) {
|
||||||
flowAnimation.current = true;
|
flowAnimation.current = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -976,6 +1025,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 动画循环
|
// 动画循环
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
|
if (isDisposingRef.current || deckLayer.isDisposedLayer()) return;
|
||||||
// 动画总时长(秒)
|
// 动画总时长(秒)
|
||||||
const animationDuration = 10;
|
const animationDuration = 10;
|
||||||
const bufferTime = 2;
|
const bufferTime = 2;
|
||||||
@@ -1075,7 +1125,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
<MapTools />
|
<MapTools />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<canvas id="deck-canvas" />
|
<canvas ref={canvasRef} />
|
||||||
</MapContext.Provider>
|
</MapContext.Provider>
|
||||||
</DataContext.Provider>
|
</DataContext.Provider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+47
-3
@@ -8,6 +8,8 @@ import { toLonLat } from "ol/proj";
|
|||||||
*/
|
*/
|
||||||
export class DeckLayer extends Layer {
|
export class DeckLayer extends Layer {
|
||||||
private deck: Deck;
|
private deck: Deck;
|
||||||
|
private canvasEl: HTMLCanvasElement | null;
|
||||||
|
private isDisposed = false;
|
||||||
private onVisibilityChange?: (layerId: string, visible: boolean) => void;
|
private onVisibilityChange?: (layerId: string, visible: boolean) => void;
|
||||||
private userVisibility: Map<string, boolean> = new Map(); // 存储用户设置的可见性
|
private userVisibility: Map<string, boolean> = new Map(); // 存储用户设置的可见性
|
||||||
|
|
||||||
@@ -15,10 +17,15 @@ export class DeckLayer extends Layer {
|
|||||||
* @param deckInstance deck.gl 实例
|
* @param deckInstance deck.gl 实例
|
||||||
* @param layerProperties 可选:在构造时直接设置到 OpenLayers Layer 的 properties
|
* @param layerProperties 可选:在构造时直接设置到 OpenLayers Layer 的 properties
|
||||||
*/
|
*/
|
||||||
constructor(deckInstance: Deck, layerProperties?: Record<string, any>) {
|
constructor(
|
||||||
|
deckInstance: Deck,
|
||||||
|
canvasElement: HTMLCanvasElement | null,
|
||||||
|
layerProperties?: Record<string, any>
|
||||||
|
) {
|
||||||
// 将 layerProperties 作为 Layer 的 properties 传入
|
// 将 layerProperties 作为 Layer 的 properties 传入
|
||||||
super({ properties: layerProperties || {} });
|
super({ properties: layerProperties || {} });
|
||||||
this.deck = deckInstance;
|
this.deck = deckInstance;
|
||||||
|
this.canvasEl = canvasElement;
|
||||||
// 再次确保属性应用到实例(兼容场景)
|
// 再次确保属性应用到实例(兼容场景)
|
||||||
if (layerProperties) {
|
if (layerProperties) {
|
||||||
this.setProperties(layerProperties);
|
this.setProperties(layerProperties);
|
||||||
@@ -38,6 +45,9 @@ export class DeckLayer extends Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(frameState: any): HTMLElement {
|
render(frameState: any): HTMLElement {
|
||||||
|
if (this.isDisposed) {
|
||||||
|
return this.canvasEl || document.createElement("div");
|
||||||
|
}
|
||||||
const { size, viewState } = frameState;
|
const { size, viewState } = frameState;
|
||||||
const [width, height] = size;
|
const [width, height] = size;
|
||||||
const [longitude, latitude] = toLonLat(viewState.center);
|
const [longitude, latitude] = toLonLat(viewState.center);
|
||||||
@@ -46,7 +56,7 @@ export class DeckLayer extends Layer {
|
|||||||
const deckViewState = { bearing, longitude, latitude, zoom };
|
const deckViewState = { bearing, longitude, latitude, zoom };
|
||||||
this.deck.setProps({ width, height, viewState: deckViewState });
|
this.deck.setProps({ width, height, viewState: deckViewState });
|
||||||
this.deck.redraw();
|
this.deck.redraw();
|
||||||
return document.getElementById("deck-canvas") as HTMLElement;
|
return this.canvasEl || document.createElement("div");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Deck 实例
|
// 获取 Deck 实例
|
||||||
@@ -56,16 +66,19 @@ export class DeckLayer extends Layer {
|
|||||||
|
|
||||||
// 设置图层
|
// 设置图层
|
||||||
setDeckLayers(layers: any[]): void {
|
setDeckLayers(layers: any[]): void {
|
||||||
|
if (this.isDisposed) return;
|
||||||
this.deck.setProps({ layers });
|
this.deck.setProps({ layers });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前图层
|
// 获取当前图层
|
||||||
getDeckLayers(): any[] {
|
getDeckLayers(): any[] {
|
||||||
|
if (this.isDisposed) return [];
|
||||||
return this.deck.props.layers || [];
|
return this.deck.props.layers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加图层
|
// 添加图层
|
||||||
addDeckLayer(layer: any): void {
|
addDeckLayer(layer: any): void {
|
||||||
|
if (this.isDisposed) return;
|
||||||
const currentLayers = this.getDeckLayers();
|
const currentLayers = this.getDeckLayers();
|
||||||
// 如果已有同 id 图层,则替换保持顺序;否则追加
|
// 如果已有同 id 图层,则替换保持顺序;否则追加
|
||||||
const idx = currentLayers.findIndex((l: any) => l && l.id === layer.id);
|
const idx = currentLayers.findIndex((l: any) => l && l.id === layer.id);
|
||||||
@@ -80,6 +93,7 @@ export class DeckLayer extends Layer {
|
|||||||
|
|
||||||
// 移除图层
|
// 移除图层
|
||||||
removeDeckLayer(layerId: string): void {
|
removeDeckLayer(layerId: string): void {
|
||||||
|
if (this.isDisposed) return;
|
||||||
const currentLayers = this.getDeckLayers();
|
const currentLayers = this.getDeckLayers();
|
||||||
const filteredLayers = currentLayers.filter(
|
const filteredLayers = currentLayers.filter(
|
||||||
(layer: any) => layer && layer.id !== layerId
|
(layer: any) => layer && layer.id !== layerId
|
||||||
@@ -97,6 +111,7 @@ export class DeckLayer extends Layer {
|
|||||||
// - 如果传入的是 Layer 实例,则直接替换同 id 的图层为该实例
|
// - 如果传入的是 Layer 实例,则直接替换同 id 的图层为该实例
|
||||||
// - 如果传入的是 props(普通对象),则基于原图层调用 clone(props)
|
// - 如果传入的是 props(普通对象),则基于原图层调用 clone(props)
|
||||||
updateDeckLayer(layerId: string, layerOrProps: any): void {
|
updateDeckLayer(layerId: string, layerOrProps: any): void {
|
||||||
|
if (this.isDisposed) return;
|
||||||
const layers = this.getDeckLayers();
|
const layers = this.getDeckLayers();
|
||||||
const updatedLayers = layers.map((layer: any) => {
|
const updatedLayers = layers.map((layer: any) => {
|
||||||
if (!layer || layer.id !== layerId) return layer;
|
if (!layer || layer.id !== layerId) return layer;
|
||||||
@@ -111,6 +126,13 @@ export class DeckLayer extends Layer {
|
|||||||
// 替换为新的 layer 实例
|
// 替换为新的 layer 实例
|
||||||
return layerOrProps;
|
return layerOrProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layerOrProps && typeof layer.clone === "function") {
|
||||||
|
// 传入 props 时,基于原图层 clone,避免丢失类型
|
||||||
|
return layer.clone(layerOrProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.deck.setProps({ layers: updatedLayers });
|
this.deck.setProps({ layers: updatedLayers });
|
||||||
@@ -137,7 +159,7 @@ export class DeckLayer extends Layer {
|
|||||||
if (!found) return;
|
if (!found) return;
|
||||||
try {
|
try {
|
||||||
// 使用 clone 来确保保持同类型实例
|
// 使用 clone 来确保保持同类型实例
|
||||||
this.updateDeckLayer(layerId, { ...found.props, visible });
|
this.updateDeckLayer(layerId, { visible });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 降级:直接替换属性
|
// 降级:直接替换属性
|
||||||
this.updateDeckLayer(layerId, { visible });
|
this.updateDeckLayer(layerId, { visible });
|
||||||
@@ -160,4 +182,26 @@ export class DeckLayer extends Layer {
|
|||||||
setLayerProperties(props: Record<string, any>): void {
|
setLayerProperties(props: Record<string, any>): void {
|
||||||
this.setProperties(props);
|
this.setProperties(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDisposedLayer(): boolean {
|
||||||
|
return this.isDisposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeDeck(): void {
|
||||||
|
if (this.isDisposed) return;
|
||||||
|
this.isDisposed = true;
|
||||||
|
this.onVisibilityChange = undefined;
|
||||||
|
this.userVisibility.clear();
|
||||||
|
try {
|
||||||
|
this.deck.setProps({ layers: [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Clear deck layers failed", error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.deck.finalize();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Finalize deck failed", error);
|
||||||
|
}
|
||||||
|
this.canvasEl = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user