import Feature from 'ol/Feature'; import Point from 'ol/geom/Point'; import OlMap from 'ol/Map'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { fromLonLat } from 'ol/proj'; import { getIconPath } from '@/utils/index'; import { getNearbyPointConfig } from '@/modules/map/domain/nearby-point-rules'; type PointLayerStyleFactory = (feature: Feature) => any; type PointLayerManagerOptions = { map: OlMap | null; createStyle: PointLayerStyleFactory; }; export class PointLayerManager { private map: OlMap | null; private createStyle: PointLayerStyleFactory; private layerRegistry: Map> = new Map(); private layerFeatureIndexes: Map< string, Map> > = new Map(); constructor(options: PointLayerManagerOptions) { this.map = options.map; this.createStyle = options.createStyle; } // 备注:同步更新地图实例,供地图初始化后把真实 map 注入点图层管理器。 setMap(map: OlMap | null) { this.map = map; } // 备注:获取当前屏幕可视区域内的所有点要素(仅包含图层可见且图例可见的要素)。 getFeaturesInViewport(): Feature[] { if (!this.map) return []; const extent = this.map.getView().calculateExtent(this.map.getSize()); if (!extent) return []; const [minX, minY, maxX, maxY] = extent; const result: Feature[] = []; this.forEachFeature((feature, layer) => { // 跳过图层本身不可见的要素 if (!layer.getVisible()) return; const geom = feature.getGeometry(); if (!geom || geom.getType() !== 'Point') return; const coords = (geom as Point).getCoordinates(); const [x, y] = coords; if (x >= minX && x <= maxX && y >= minY && y <= maxY) { result.push(feature); } }); return result; } // 备注:统一返回当前点图层注册表,供外层做遍历或清理。 getRegistry() { return this.layerRegistry; } // 备注:按图层 key 获取已注册的点图层实例。 getLayer(layerKey: string) { return this.layerRegistry.get(layerKey); } // 备注:返回当前所有点图层实例,供命中检测和批量刷新使用。 getLayers(): VectorLayer[] { return Array.from(this.layerRegistry.values()); } // 备注:按当前缩放级别统一刷新近邻点展开坐标,保证抽吸后的点击和 popup 与视觉位置一致。 updateNearbyFeatureLayout(currentZoom?: number): void { if (!this.map) return; const resolution = this.map.getView().getResolution(); if ( currentZoom === undefined || !Number.isFinite(currentZoom) || !resolution ) { return; } const nearbyPointConfig = getNearbyPointConfig(); const { autoRule, layoutRule } = nearbyPointConfig; const groupMap = new Map(); this.forEachFeature(feature => { this.resetFeatureToBaseCoordinate(feature); const groupId = feature.get('_nearbyGroupId'); const groupSize = Number(feature.get('_nearbyGroupSize')); if (!groupId || !Number.isFinite(groupSize) || groupSize < 2) { return; } const groupFeatures = groupMap.get(groupId) || []; groupFeatures.push(feature); groupMap.set(groupId, groupFeatures); }); groupMap.forEach(features => { const expandAtZoom = Number(features[0]?.get('_nearbyExpandAtZoom')); if (!Number.isFinite(expandAtZoom) || currentZoom < expandAtZoom) { return; } const sortedFeatures = [...features].sort((left, right) => { return ( Number(left.get('_nearbyPriority') || 0) - Number(right.get('_nearbyPriority') || 0) ); }); const secondaryFeatures = sortedFeatures.filter(feature => { const priority = Number(feature.get('_nearbyPriority')); return Number.isFinite(priority) && priority > 1; }); secondaryFeatures.forEach((feature, index) => { const baseCoordinates = feature.get('_baseCoordinates') as | number[] | undefined; const geometry = feature.getGeometry(); if ( !baseCoordinates || baseCoordinates.length < 2 || !geometry || geometry.getType() !== 'Point' ) { return; } const groupSecondaryCount = secondaryFeatures.length; const baseOffsetPx = Number(feature.get('_nearbyExpandOffsetPx')) || autoRule.expandOffsetPx; const ringSize = Math.max(1, layoutRule.ringSize); const ringIndex = Math.floor(index / ringSize); const ringOffsetIndex = index % ringSize; const currentRingCount = Math.min( ringSize, groupSecondaryCount - ringIndex * ringSize ); const fanAngleDeg = currentRingCount <= 1 ? 0 : currentRingCount <= 2 ? layoutRule.fanAngleForTwoDeg : currentRingCount <= 4 ? layoutRule.fanAngleForFourDeg : layoutRule.fanAngleForManyDeg; const angleStepDeg = currentRingCount <= 1 ? 0 : fanAngleDeg / (currentRingCount - 1); const startAngleDeg = -90 - fanAngleDeg / 2; const expandAngleDeg = startAngleDeg + ringOffsetIndex * angleStepDeg; const expandAngleRad = (expandAngleDeg * Math.PI) / 180; const expandOffsetPx = baseOffsetPx + ringIndex * Math.max( layoutRule.ringGapPx, baseOffsetPx * layoutRule.ringGapFactor ); const offsetX = Math.cos(expandAngleRad) * expandOffsetPx * resolution; const offsetY = Math.sin(expandAngleRad) * expandOffsetPx * resolution; feature.set('_nearbyExpandAngleDeg', expandAngleDeg, true); (geometry as Point).setCoordinates([ baseCoordinates[0] + offsetX, baseCoordinates[1] - offsetY ]); }); }); } // 备注:判断指定图层 key 的点图层是否已存在。 hasLayer(layerKey: string): boolean { return this.layerRegistry.has(layerKey); } // 备注:按需获取或创建点图层实例,并自动完成注册与挂载。 ensureLayer(layerKey: string, layerType?: string) { if (!this.map || !layerKey) return null; let vectorLayer = this.layerRegistry.get(layerKey); if (vectorLayer) { return vectorLayer; } const vectorSource = new VectorSource(); vectorLayer = new VectorLayer({ source: vectorSource, zIndex: 100, declutter: true, style: feature => this.createStyle(feature as Feature) }); (vectorLayer as any).type = layerType || layerKey; this.layerRegistry.set(layerKey, vectorLayer); this.map.addLayer(vectorLayer); return vectorLayer; } // 备注:把后端点位数组转换为 OpenLayers Feature,并写入指定点图层。 addDataLayer(pointData: any, layerType: string): void { if (!this.map) return; let dataArray: any[] = []; let targetLayerKey = layerType; if (Array.isArray(pointData)) { dataArray = pointData; } else { dataArray = pointData?.data || []; targetLayerKey = pointData?.key || layerType; } if (!targetLayerKey) { console.warn('缺少图层 Key,无法加载描点'); return; } const vectorLayer = this.ensureLayer(targetLayerKey, layerType); if (!vectorLayer) return; const vectorSource = vectorLayer.getSource(); if (!vectorSource) { return; } vectorSource.clear(); this.resetLayerFeatureIndexes(targetLayerKey); if (dataArray.length === 0) { return; } const features: Feature[] = []; dataArray.forEach((item: any) => { const feature = this.createFeature(item, targetLayerKey); if (feature) { this.indexFeature(targetLayerKey, feature); features.push(feature); } }); if (features.length > 0) { vectorSource.addFeatures(features); } } // 备注:统一控制整个点图层显隐,供图层树勾选联动使用。 setLayerVisible(layerKey: string, visible?: boolean): void { const vectorLayer = this.layerRegistry.get(layerKey); if (vectorLayer && vectorLayer.getVisible() !== visible) { vectorLayer.setVisible(visible); vectorLayer.changed(); } } // 备注:按图层 key 删除点图层并清空其 source 数据。 removeLayer(layerKey: string): void { if (!this.map || !layerKey) { console.warn('removePointLayer: 无效的图层 key 或地图未初始化'); return; } const vectorLayer = this.layerRegistry.get(layerKey); if (!vectorLayer) { console.warn(`未找到标识为 [${layerKey}] 的描点图层实例`); return; } const source = vectorLayer.getSource(); if (source) { source.clear(); } this.map.removeLayer(vectorLayer); this.layerRegistry.delete(layerKey); this.layerFeatureIndexes.delete(layerKey); } // 备注:按图层遍历所有点图层实例,供 hover 刷新和区域裁切复用。 forEachLayer( callback: (layer: VectorLayer, layerKey: string) => void ): void { this.layerRegistry.forEach((layer, layerKey) => { callback(layer, layerKey); }); } // 备注:遍历所有点要素,供区域裁切或批量状态更新使用。 forEachFeature( callback: ( feature: Feature, layer: VectorLayer, layerKey: string ) => void ): void { this.forEachLayer((layer, layerKey) => { const source = layer.getSource(); if (!source) return; source.getFeatures().forEach(feature => { callback(feature as Feature, layer, layerKey); }); }); } // 备注:按图例字段值统一控制要素显隐,供图例和容量筛选联动复用。 setLegendVisibleByField( layerKey: string, matchValue: string, checked: boolean, fieldKey = 'anchoPointState' ): void { const vectorLayer = this.layerRegistry.get(layerKey); if (!vectorLayer) return; const source = vectorLayer.getSource(); if (!source) return; if (!matchValue) { source.getFeatures().forEach(feature => { if (feature.get('_legendVisible') !== false) { feature.set('_legendVisible', false); feature.changed(); } }); vectorLayer.changed(); return; } const targetFeatures = this.getIndexedFeatures( layerKey, fieldKey, matchValue ); if (targetFeatures.length === 0) return; targetFeatures.forEach(feature => { if (feature.get('_legendVisible') !== checked) { feature.set('_legendVisible', checked); feature.changed(); } }); vectorLayer.changed(); } // 备注:统一销毁所有点图层并清空注册表,供地图销毁阶段复用。 destroy(): void { this.forEachLayer(layer => { const source = layer.getSource(); if (source) { source.clear(); } if (this.map && this.map.getLayers().getArray().includes(layer)) { this.map.removeLayer(layer); } if ((layer as any).setMap) { (layer as any).setMap(null); } }); this.layerRegistry.clear(); this.layerFeatureIndexes.clear(); } // 备注:统一把业务点位数据转换为 Feature,并补齐样式依赖字段。 private createFeature(item: any, targetLayerKey: string): Feature | null { const { lgtd, lttd, stnm, iconCode, titleName, ennm } = item; if (lgtd == null || lttd == null) { return null; } const lon = Number(lgtd); const lat = Number(lttd); if (isNaN(lon) || isNaN(lat) || !isFinite(lon) || !isFinite(lat)) { return null; } if (Math.abs(lon) > 180 || Math.abs(lat) > 90) { return null; } const coord = fromLonLat([lon, lat]); if (!isFinite(coord[0]) || !isFinite(coord[1])) { return null; } let iconUrl = iconCode ? getIconPath(iconCode) : ''; if (!iconUrl) { iconUrl = getIconPath('default') || ''; } const feature = new Feature({ geometry: new Point(coord), ...item, _layerKey: targetLayerKey }); feature.setId(item.stcd || item._id); feature.set('_iconUrl', iconUrl); feature.set( '_labelText', item.sttpMap === 'ylfb' ? item.ftp || '' : titleName || stnm || ennm || '' ); feature.set('_baseCoordinates', coord); feature.set('_legendVisible', true); feature.set('_regionVisible', true); feature.set('_sttpMap', item.sttpMap); if (item.popupHtml) { feature.set('popupHtml', item.popupHtml); } return feature; } // 备注:重置单个图层的字段索引,供重新灌入数据或删除图层时复用。 private resetLayerFeatureIndexes(layerKey: string): void { this.layerFeatureIndexes.set(layerKey, new Map()); } // 备注:按字段值给要素建立索引,供图例切换时快速命中目标锚点集合。 private indexFeature(layerKey: string, feature: Feature): void { const fieldIndexMap = this.layerFeatureIndexes.get(layerKey) || new Map>(); const legendFieldKey = 'anchoPointState'; const legendFieldValue = feature.get(legendFieldKey); if (legendFieldValue != null) { const valueMap = fieldIndexMap.get(legendFieldKey) || new Map(); const normalizedValue = String(legendFieldValue); const targetFeatures = valueMap.get(normalizedValue) || []; targetFeatures.push(feature); valueMap.set(normalizedValue, targetFeatures); fieldIndexMap.set(legendFieldKey, valueMap); } this.layerFeatureIndexes.set(layerKey, fieldIndexMap); } // 备注:根据图层 key、字段名和值快速返回目标要素集合,避免图例切换时整层遍历。 private getIndexedFeatures( layerKey: string, fieldKey: string, matchValue: string ): Feature[] { const fieldIndexMap = this.layerFeatureIndexes.get(layerKey); if (!fieldIndexMap) return []; const valueMap = fieldIndexMap.get(fieldKey); if (!valueMap) return []; return valueMap.get(String(matchValue)) || []; } // 备注:先恢复要素原始坐标,再按缩放阶段决定是否做展开偏移,避免多次缩放后偏移累计。 private resetFeatureToBaseCoordinate(feature: Feature): void { const baseCoordinates = feature.get('_baseCoordinates') as | number[] | undefined; const geometry = feature.getGeometry(); if ( !baseCoordinates || baseCoordinates.length < 2 || !geometry || geometry.getType() !== 'Point' ) { return; } (geometry as Point).setCoordinates([ baseCoordinates[0], baseCoordinates[1] ]); } }