import OlMap from 'ol/Map'; import View from 'ol/View'; import TileLayer from 'ol/layer/Tile'; import GeoJSON from 'ol/format/GeoJSON'; import VectorSource from 'ol/source/Vector'; import { fromLonLat } from 'ol/proj'; import { PointLayerManager } from './point-layer-manager'; type RegionMaskManagerOptions = { map: OlMap | null; view: View | null; pointLayerManager: PointLayerManager; defaultCenter: [number, number]; defaultZoom: number; }; export class RegionMaskManager { private map: OlMap | null; private view: View | null; private pointLayerManager: PointLayerManager; private defaultCenter: [number, number]; private defaultZoom: number; private maskBindings: Array<{ layer: TileLayer; prerender: (event: any) => void; postrender: (event: any) => void; }> = []; constructor(options: RegionMaskManagerOptions) { this.map = options.map; this.view = options.view; this.pointLayerManager = options.pointLayerManager; this.defaultCenter = options.defaultCenter; this.defaultZoom = options.defaultZoom; } // 备注:同步地图和视图上下文,供地图初始化、销毁和重建后复用区域过滤能力。 setContext(map: OlMap | null, view: View | null) { this.map = map; this.view = view; } // 备注:统一隐藏所有点位的区域显示状态,供基地裁切前的预处理复用。 hideAllPoints(): void { this.pointLayerManager.forEachFeature(feature => { feature.set('_regionVisible', false); feature.changed(); }); } // 备注:统一恢复所有点位的区域显示状态,供取消裁切或异常兜底时复用。 showAllPoints(): void { this.pointLayerManager.forEachFeature(feature => { feature.set('_regionVisible', true); feature.changed(); }); } // 备注:根据 GeoJSON 边界批量更新点位的区域显隐状态。 filterPointsByRegion(geoJson: any): void { const regionCoords = this.extractPolygonCoords(geoJson); if (!regionCoords.length) { console.warn('无法解析区域边界,显示所有锚点'); this.showAllPoints(); return; } this.pointLayerManager.forEachFeature(feature => { const props = feature.getProperties(); const lon = Number(props.lgtd); const lat = Number(props.lttd); if (!isFinite(lon) || !isFinite(lat)) { feature.set('_regionVisible', false); feature.changed(); return; } feature.set('_regionVisible', this.isPointInPolygon(lon, lat, regionCoords)); feature.changed(); }); } // 备注:根据 GeoJSON 范围拟合地图视野,供基地切换后快速聚焦到目标区域。 fitViewToGeoJson(geoJson: any): void { if (!this.map || !this.view) return; try { const features = new GeoJSON().readFeatures(geoJson, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }); if (!features.length) return; const source = new VectorSource({ features }); const extent = source.getExtent(); if (extent[0] === Infinity || extent[0] === -Infinity) { console.warn('无法计算有效的地图包围盒'); return; } this.view.fit(extent, { padding: [100, 200, 50, 50], duration: 1000, maxZoom: 13 }); } catch (error) { console.error('调整地图视野失败:', error); } } // 备注:把 GeoJSON 边界绑定到底图 prerender/postrender 事件,实现区域裁切遮罩。 applyMapMask(rasterLayer: TileLayer, clipGeoJson: any): void { this.applyMapMaskToLayers(rasterLayer ? [rasterLayer] : [], clipGeoJson); } // 备注:把 GeoJSON 边界同时绑定到多个底图图层,保证多底图叠加场景下裁切边界保持一致。 applyMapMaskToLayers(rasterLayers: TileLayer[] = [], clipGeoJson: any): void { if (!clipGeoJson) return; const targetLayers = rasterLayers.filter(Boolean); if (targetLayers.length === 0) return; const features = new GeoJSON().readFeatures(clipGeoJson, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }); if (!features.length) { console.warn('裁切数据为空,无法应用遮罩'); return; } this.clearMaskEvents(); targetLayers.forEach(rasterLayer => { const maskPrerender = (event: any) => { const context = event.context; const frameState = event.frameState; if (!context || !frameState || !this.map) return; context.save(); context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); let hasValidPath = false; for (const feature of features) { const geometry = feature.getGeometry(); if (!geometry) continue; if (geometry.getType() === 'Polygon') { const rings = (geometry as any).getCoordinates() as number[][][]; for (const ring of rings) { if (this.drawRingToContext(context, ring, frameState)) { hasValidPath = true; } } } else if (geometry.getType() === 'MultiPolygon') { const polygons = (geometry as any).getCoordinates() as number[][][][]; for (const polygonRings of polygons) { for (const ring of polygonRings) { if (this.drawRingToContext(context, ring, frameState)) { hasValidPath = true; } } } } } if (!hasValidPath) { console.warn('未能生成有效的裁切路径'); return; } context.save(); context.lineWidth = 6; context.strokeStyle = '#6D64DF'; context.stroke(); context.clip(); context.strokeStyle = '#CCC9F4'; context.lineWidth = 5; context.stroke(); context.clip(); }; const maskPostrender = (event: any) => { if (event.context) { event.context.restore(); event.context.restore(); } }; this.maskBindings.push({ layer: rasterLayer, prerender: maskPrerender, postrender: maskPostrender }); rasterLayer.on('prerender', maskPrerender); rasterLayer.on('postrender', maskPostrender); rasterLayer.changed(); }); } // 备注:清除当前底图遮罩并按需恢复默认视野,供切换基地和异常兜底复用。 clearMapMask(resetView = true): void { const maskedLayers = this.maskBindings.map(binding => binding.layer); this.clearMaskEvents(); maskedLayers.forEach(layer => layer.changed()); if (resetView && this.view) { this.view.animate({ center: fromLonLat(this.defaultCenter), zoom: this.defaultZoom, duration: 1000 }); } } // 备注:统一销毁区域过滤相关引用和事件,供地图销毁阶段复用。 destroy(): void { this.clearMapMask(false); this.map = null; this.view = null; } // 备注:从 GeoJSON 中提取可用于点在面判断的多边形环坐标。 private extractPolygonCoords(geoJson: any): number[][][] { if (!geoJson?.features?.length) { return []; } const coords: number[][][] = []; geoJson.features.forEach((feature: any) => { const geometry = feature.geometry; if (!geometry) return; if (geometry.type === 'Polygon') { coords.push(...geometry.coordinates); } else if (geometry.type === 'MultiPolygon') { geometry.coordinates.forEach((polygon: number[][][]) => { coords.push(...polygon); }); } }); return coords; } // 备注:使用射线法判断点是否落在任意一个区域环内。 private isPointInPolygon( lon: number, lat: number, polygons: number[][][] ): boolean { for (const polygon of polygons) { if (this.isPointInRing(lon, lat, polygon)) { return true; } } return false; } // 备注:使用射线法判断点是否落在单个闭合环内。 private isPointInRing(lon: number, lat: number, ring: number[][]): boolean { let inside = false; const ringLength = ring.length; for (let i = 0, j = ringLength - 1; i < ringLength; j = i++) { const xi = ring[i][0]; const yi = ring[i][1]; const xj = ring[j][0]; const yj = ring[j][1]; const intersect = yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi; if (intersect) { inside = !inside; } } return inside; } // 备注:把单个面环转换为当前帧 Canvas 可裁切的路径。 private drawRingToContext( context: CanvasRenderingContext2D, ring: number[][], frameState: any ): boolean { if (!ring || ring.length < 3) return false; const pixelRatio = frameState.pixelRatio || window.devicePixelRatio || 1; let moved = false; for (let i = 0; i < ring.length; i++) { const coord = ring[i]; if (!coord || coord.length < 2 || typeof coord[0] !== 'number') { continue; } const cssPixel = this.map?.getPixelFromCoordinate(coord as [number, number]); if (!cssPixel) continue; const canvasX = cssPixel[0] * pixelRatio; const canvasY = cssPixel[1] * pixelRatio; if (!moved) { context.moveTo(canvasX, canvasY); moved = true; } else { context.lineTo(canvasX, canvasY); } } if (moved) { context.closePath(); return true; } return false; } // 备注:解除旧底图上的遮罩事件,避免重复绑定或残留旧裁切效果。 private clearMaskEvents(): void { this.maskBindings.forEach(binding => { binding.layer.un('prerender', binding.prerender); binding.layer.un('postrender', binding.postrender); }); this.maskBindings = []; } }