import { MapInterface } from './map'; import OlMap from 'ol/Map'; import View from 'ol/View'; import Overlay from 'ol/Overlay'; import TileLayer from 'ol/layer/Tile'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import GeoJSON from 'ol/format/GeoJSON'; import Style from 'ol/style/Style'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Icon from 'ol/style/Icon'; import Text from 'ol/style/Text'; import Circle from 'ol/style/Circle'; import RegularShape from 'ol/style/RegularShape'; import Cluster from 'ol/source/Cluster'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; import { get as getProjection, fromLonLat } from 'ol/proj'; import { defaults as defaultInteractions, Draw, DoubleClickZoom } from 'ol/interaction'; import { getTopLeft, getWidth } from 'ol/extent'; import MouseWheelZoom from 'ol/interaction/MouseWheelZoom'; import { servers } from './mapurlManage'; import { XYZ } from 'ol/source'; import Feature from 'ol/Feature'; import LineString from 'ol/geom/LineString'; import { getLength as getSphericalLength, getArea as getSphericalArea } from 'ol/sphere'; import { unByKey } from 'ol/Observable'; import { MDOptions } from './map.class'; import { useModelStore } from '@/store/modules/model'; import { PointLayerManager } from './ol/point-layer-manager'; import { PopupManager } from './ol/popup-manager'; import { RegionMaskManager } from './ol/region-mask-manager'; const modelStore = useModelStore(); const VITE_APP_MAP_URL = import.meta.env.VITE_APP_MAP_URL; // 定义与 leaflet 中相同的常量 const CENTER_positionCN = [114.17112499999996, 38]; // OpenLayers 使用 [lon, lat] const MIN_ZOOM = 4.23; const MAX_ZOOM = 22; const INITIAL_ZOOM = 4.5; // 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标) const BOUNDS_SW = [26.5, -9.99999999999929]; const BOUNDS_NE = [180.00000000000074, 60.06349386538693]; export class MapOl implements MapInterface { map: OlMap | null = null; view: View | null = null; private layerRegistry: Map = new Map(); private baseLayerConfig: any | null = null; private hydropBaseConfig: any | null = null; private REGISTRY_KEY = 'customBaseLayer'; private activeBaseLayerKey = 's_province_boundaries'; private readonly rasterBaseLayerKeys = ['BASEMAP-white', 'BASEMAP-img']; private drawInteraction: any = null; private measureLayer: VectorLayer | null = null; private measureSource: VectorSource | null = null; private pointLayerManager: PointLayerManager; geoJsonData: any = null; geoJsonData1: any = null; private BASEID = ''; private currentClipGeoJson: any | null = null; private hoveredFeatureId: string | number | null = null; private popupManager: PopupManager; private regionMaskManager: RegionMaskManager; constructor() { this.pointLayerManager = new PointLayerManager({ map: null, createStyle: feature => this.createPointStyle(feature) }); this.popupManager = new PopupManager({ map: null, getPointLayers: () => this.pointLayerManager.getLayers() }); this.regionMaskManager = new RegionMaskManager({ map: null, view: null, pointLayerManager: this.pointLayerManager, defaultCenter: CENTER_positionCN as [number, number], defaultZoom: INITIAL_ZOOM }); } private async loadGeoJsonData(url): Promise { try { const response = await fetch(url); const data = await response.json(); return data; } catch (error) { console.error('配置加载失败:', error); } } //地图初始化 init(container: HTMLElement): Promise { try { const minCoordinate = fromLonLat(BOUNDS_SW); const maxCoordinate = fromLonLat(BOUNDS_NE); const extent = [ minCoordinate[0], minCoordinate[1], maxCoordinate[0], maxCoordinate[1] ]; this.view = new View({ center: fromLonLat(CENTER_positionCN), // 设置中心点 zoom: INITIAL_ZOOM, minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM, constrainOnlyCenter: false, // 允许部分视图超出边界,但中心点受限(类似 Leaflet 默认行为) extent: extent, // 限制视图范围 smoothExtentConstraint: false // 平滑边界约束 }); const mouseWheelInteraction = new MouseWheelZoom({ duration: 100, // 缩放动画持续时间 (ms),Leaflet 默认也有动画 maxDelta: 2, constrainResolution: false // ✅ 关键:false 允许缩放到非整数级别 (如 4.5, 4.6),实现平滑缩放 }); this.map = new OlMap({ target: container, layers: [], view: this.view, controls: [], // 对应 Leaflet 的 attributionControl: false, zoomControl: false interactions: defaultInteractions({ doubleClickZoom: true, dragPan: true, pinchRotate: false // 通常禁用旋转,除非需要 }).extend([mouseWheelInteraction]) }); this.pointLayerManager.setMap(this.map); this.popupManager.setMap(this.map); this.regionMaskManager.setContext(this.map, this.view); this.view.on('change:resolution', () => {}); this.map.on('click', evt => { this.popupManager.showPopup(undefined, undefined); this.popupManager.handleMapClick(evt.pixel, detectedFeature => { console.log(detectedFeature); modelStore.modalVisible = true; modelStore.params = detectedFeature.values_; modelStore.title = detectedFeature.values_.titleName || detectedFeature.values_.stnm + ' 详情信息'; }); }); this.map.on('pointermove', evt => { this.popupManager.handlePointerMove(evt.pixel, payload => { this.hoveredFeatureId = payload.hoveredId; const targetElement = this.map?.getTargetElement() as HTMLElement; if (targetElement) { targetElement.style.cursor = payload.hoveredId ? 'pointer' : ''; } this.showPopup(payload.detectedFeature, payload.coordinate); }); }); return Promise.resolve(this.map); } catch (e) { console.error('OL Init Error', e); return Promise.reject(e); } } /** * 初始化加载描点数据 * @param pointData 图层配置对象或数据数组 * @param layerType 图层类型/Key * @param mdoptions 描点选项配置 */ addInitDataLayer( pointData: any, layerType: any, _mdoptions?: MDOptions ): void { this.pointLayerManager.addDataLayer(pointData, layerType); } /** * 初始化 Popup Overlay * @param container 父组件传入的 DOM 容器 */ initPopupOverlay(container: HTMLElement) { this.popupManager.initPopupOverlay(container); } /** * 在指定像素位置检测要素(共享方法) * @param pixel 像素坐标 * @returns 检测到的要素和是否命中图标区域 */ private detectFeatureAtPixel(pixel: number[]): { detectedFeature: Feature | undefined; isHitIcon: boolean; coordinate?: number[]; } { return this.popupManager.detectFeatureAtPixel(pixel); } /** * 在 pointermove 中调用此方法显示/隐藏 Popup * @param feature 当前悬停的要素 * @param coordinate 地理坐标 */ showPopup(feature: Feature | undefined, coordinate: number[] | undefined) { this.popupManager.showPopup(feature, coordinate); } /** * 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小) */ private createPointStyle(feature: Feature): Style { const iconUrl = feature.get('_iconUrl') as string; const labelText = feature.get('_labelText') as string; const legendVisible = feature.get('_legendVisible'); const regionVisible = feature.get('_regionVisible'); if (!iconUrl) { return null; } if (legendVisible === false) { return null; } if (regionVisible === false) { return null; } const currentZoom: any = this.view ? this.view.getZoom() : 4.5; if (!this.shouldRenderFeatureByDistance(feature, currentZoom)) { return null; } const dynamicScale = this.getDynamicPointScale(currentZoom); const fontSize = this.getPointFontSize(dynamicScale); const styleOptions: any = { image: new Icon({ src: iconUrl, scale: dynamicScale, anchor: [0.5, 0.5], crossOrigin: 'anonymous', declutterMode: 'declutter' }) }; styleOptions.text = new Text({ text: labelText, offsetY: -20 * dynamicScale, font: `${fontSize}px sans-serif`, fill: new Fill({ color: '#fff' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }), textAlign: 'center', declutterMode: 'declutter' }); return new Style(styleOptions); } /** * 根据缩放级别计算锚点图标缩放比例,统一复用,减少样式函数中的重复判断。 */ private getDynamicPointScale(currentZoom: number): number { let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08; dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale)); return dynamicScale; } /** * 根据图标缩放比例统一计算文字字号,避免样式函数中散落重复计算。 */ private getPointFontSize(dynamicScale: number): number { return Math.max(10, Math.min(24, 12 * dynamicScale)); } /** * 统一根据缩放级别和点位距离判断要素是否需要参与渲染,减少样式函数中的业务判断复杂度。 */ private shouldRenderFeatureByDistance( feature: Feature, currentZoom: number ): boolean { const distance = feature.get('distance'); if (distance === undefined || distance === null) { return true; } let minDistanceRequired = 0; if (currentZoom <= 6) { minDistanceRequired = 1500000; } else if (currentZoom <= 8) { minDistanceRequired = 800000; } else if (currentZoom <= 10.5) { minDistanceRequired = 400000; } return distance > minDistanceRequired; } private createPointStyle1(feature: Feature): Style { const color = (feature.get('iconCode') as string) || '#3399CC'; const labelText = feature.get('titleName') as string; const buildType = (feature.get('BuildType') as number) ?? 1; const currentZoom = this.view ? this.view.getZoom() : 4.5; let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08; dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale)); const fontSize = Math.max(16, Math.min(24, 12 * dynamicScale)); const radius = Math.max(6, Math.min(18, 6 * dynamicScale)); let imageStyle; if (buildType === 0) { const canvas = this.createSemiCircleCanvas(radius, color); imageStyle = new Icon({ img: canvas, imgSize: [canvas.width, canvas.height], anchor: [0.5, 0.5] }); } else { imageStyle = new Circle({ radius: radius, fill: new Fill({ color: color }), stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.5)', width: 1 }) }); } const styleOptions: any = { image: imageStyle }; if (labelText) { let offsetY = -20 * dynamicScale; const specialLabels = [ '枕头坝二级', '老鹰岩二级', '长洲', '丹巴', '玛尔挡', '班多', '彭水', '李家峡', 'ML', 'BDa', '乌东德', '里底', '大华桥', '巴塘' ]; if (specialLabels.includes(labelText)) { offsetY = 20 * dynamicScale; } styleOptions.text = new Text({ text: labelText, offsetY: offsetY, font: `${fontSize}px sans-serif`, fill: new Fill({ color: '#fff' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }), textAlign: 'center', declutterMode: 'declutter' }); } return new Style(styleOptions); } /** * 初始化加载基础图层 * @param layer 图层配置对象 */ addBaseDataLayer( layer: any, isShow: boolean, isCache: boolean = false ): void { if (!this.map) return; // 备注:同 key 的底图重复添加前先移除旧实例,避免注册表覆盖后旧图层仍残留在地图上。 const existingLayer = layer?.key ? this.layerRegistry.get(layer.key as string) : null; if (existingLayer) { this.map.removeLayer(existingLayer); this.layerRegistry.delete(layer.key); } if (layer.type === 'wmts') { if (!layer.url) return; let url = !isCache ? layer.url : layer.url_3d; const urlParams = new URLSearchParams(url.split('?')[1]); if (layer.key === this.REGISTRY_KEY) { this.baseLayerConfig = layer; } const layerName: string = urlParams.get('LAYER') || ''; const matrixSetName: any = urlParams.get('TILEMATRIXSET'); const projection = getProjection('EPSG:3857'); if (!projection) { console.error('无法获取 EPSG:3857 投影'); return; } const projectionExtent = projection.getExtent(); const size = getWidth(projectionExtent) / 256; const maxZoom = 13; const resolutions = new Array(maxZoom + 1); const matrixIds = new Array(maxZoom + 1); for (let z = 0; z <= maxZoom; ++z) { resolutions[z] = size / Math.pow(2, z); matrixIds[z] = `${matrixSetName}:${z}`; } const wmtsLayer = new TileLayer({ source: new WMTS({ url: layer.url.split('?')[0], layer: layerName, matrixSet: matrixSetName, format: 'image/png', projection: projection, tileGrid: new WMTSTileGrid({ origin: getTopLeft(projectionExtent), resolutions: resolutions, matrixIds: matrixIds }), style: 'default', wrapX: true, crossOrigin: 'anonymous' }) }); if (layer.key === this.REGISTRY_KEY) { !isCache ? wmtsLayer.setZIndex(-100) : wmtsLayer.setZIndex(-99); } else { wmtsLayer.setZIndex(-100); } wmtsLayer.type = 'layer'; wmtsLayer.setVisible(isShow); this.layerRegistry.set(layer.key, wmtsLayer); this.map.addLayer(wmtsLayer); layer._layer = wmtsLayer; } else if (layer.type == 'raster-dem') { const tileLayer = new TileLayer({ source: new XYZ({ url: layer.url, wrapX: true, crossOrigin: 'anonymous' }) }); tileLayer.setZIndex(-100); tileLayer.setVisible(isShow); tileLayer.type = 'layer'; this.layerRegistry.set(layer.key, tileLayer); this.map.addLayer(tileLayer); } else if (layer.type === 'vector') { if (layer.key === 'hydropBase') { this.hydropBaseConfig = layer; } } } /** * 根据 key 设置底图图层显隐 * @param layerKey 图层 key * @param visible 是否显示 */ private setBaseLayerVisible(layerKey: string, visible: boolean): void { const layer = this.layerRegistry.get(layerKey); if (layer) { layer.setVisible(visible); } } /** * 同步当前激活底图源的显隐,保证树勾选和下方图源切换使用同一套可见性规则 * @param checked 是否显示当前激活底图 */ private syncActiveBaseLayerVisibility(checked: boolean): void { this.setBaseLayerVisible(this.REGISTRY_KEY, checked); this.rasterBaseLayerKeys.forEach(key => { const shouldShow = checked && this.activeBaseLayerKey !== 's_province_boundaries' && key === this.activeBaseLayerKey; this.setBaseLayerVisible(key, shouldShow); }); } /** * 获取当前应应用基地裁切的底图图层集合 * @returns 当前可见底图图层列表 */ private getCurrentMaskTargetLayers(): TileLayer[] { const layers: TileLayer[] = []; const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY); if (baseLayer instanceof TileLayer) { layers.push(baseLayer); } if (this.activeBaseLayerKey !== 's_province_boundaries') { const activeRasterLayer = this.layerRegistry.get(this.activeBaseLayerKey); if ( activeRasterLayer instanceof TileLayer && activeRasterLayer !== baseLayer ) { layers.push(activeRasterLayer); } } return layers; } /** * 重新把当前基地裁切绑定到最新底图实例,保证切换图源后边界裁切持续生效 */ private reapplyCurrentBaseMask(): void { if (!this.currentClipGeoJson) { return; } const targetLayers = this.getCurrentMaskTargetLayers(); if (targetLayers.length === 0) { return; } this.regionMaskManager.applyMapMaskToLayers( targetLayers, this.currentClipGeoJson ); } enableNortheastMask(): void { const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY); if (!this.geoJsonData1) { console.warn('东北边界数据尚未加载'); return; } if (baseLayer && baseLayer instanceof TileLayer) { this.regionMaskManager.applyMapMask(baseLayer, this.geoJsonData1); } else { console.warn('未找到全量底图图层或图层类型错误'); } } /** * 控制特定区域图层的显示(互斥显示) * @param regionId 区域ID (例如 "hebei", "13", "01" 等,需与图层 Key 或属性对应) * @param isAll 是否显示所有 (true: 显示所有图层; false: 仅显示匹配 regionId 的图层,隐藏其他) */ async jdPanelControlShowAndHidden( _regionId: string, isAll: boolean ): Promise { if (!this.map || !this.hydropBaseConfig) { console.warn('地图未初始化或 hydropBaseConfig 未配置'); return; } this.BASEID = _regionId; if (!isAll) { this.currentClipGeoJson = null; this.regionMaskManager.clearMapMask(); this.regionMaskManager.showAllPoints(); return; } const baseLayers = this.getCurrentMaskTargetLayers(); if (baseLayers.length === 0) { console.warn('未找到底图图层,无法应用遮罩'); return; } this.regionMaskManager.hideAllPoints(); try { const url = this.hydropBaseConfig.geojson_url + `&cql_filter=BASEID='${this.BASEID}'`; const geoJsonData = await this.loadGeoJsonData(url); this.currentClipGeoJson = geoJsonData; this.regionMaskManager.filterPointsByRegion(geoJsonData); this.regionMaskManager.fitViewToGeoJson(geoJsonData); this.regionMaskManager.applyMapMaskToLayers(baseLayers, geoJsonData); } catch (error) { this.currentClipGeoJson = null; console.error('加载裁切数据失败:', error); this.regionMaskManager.clearMapMask(); this.regionMaskManager.showAllPoints(); } } /** * 基础图层显示影隐藏方法 * @param layerType 图层类型 (备用,优先使用 key) * @param key 图层唯一标识 Key * @param checked true 为显示,false 为隐藏 */ controlBaseLayerTreeShowAndHidden( layerType: string, key: string, checked: boolean ): void { if (!this.map) return; const registryKey = key || layerType; if (!registryKey) { console.warn( 'controlBaseLayerTreeShowAndHidden: 缺少有效的 Key 或 LayerType' ); return; } if (registryKey === this.REGISTRY_KEY) { this.syncActiveBaseLayerVisibility(checked); return; } const layerInstance = this.layerRegistry.get(registryKey as string); if (layerInstance) { layerInstance.setVisible(checked); } else { console.warn( `未找到标识为 [${registryKey}] 的图层实例,当前注册表 keys:`, Array.from(this.layerRegistry.keys()) ); } } mdLayerTreeShowOrHidden(layerType: string, checked?: boolean): void { this.pointLayerManager.setLayerVisible(layerType, checked); } hasLayer(layerType: string): void { return this.pointLayerManager.hasLayer(layerType as string); } /** * 缩放地图 * @param type 'out' 缩小, 'in' 放大 */ zoomToggle(type: 'out' | 'in'): void { if (!this.map || !this.view) return; const currentZoom = this.view.getZoom(); if (currentZoom === undefined) return; // 定义缩放步长,对应 Leaflet 的 zoomDelta const zoomDelta = 1; let targetZoom = currentZoom; if (type === 'in') { targetZoom = currentZoom + zoomDelta; // 限制最大缩放级别 if (targetZoom > MAX_ZOOM) { targetZoom = MAX_ZOOM; } } else { targetZoom = currentZoom - zoomDelta; // 限制最小缩放级别 if (targetZoom < MIN_ZOOM) { targetZoom = MIN_ZOOM; } } // 使用 animate 实现平滑缩放动画 this.view.animate({ zoom: targetZoom, duration: 250, // 动画持续时间 (ms),与滚轮缩放保持一致 easing: t => t // 线性缓动,也可以引入 ol/easing 使用更复杂的曲线 }); } /** * 切换底图 * @param key 业务标识 Key (如 's_province_boundaries', 'BASEMAP-white', 'BASEMAP-img') */ baseLayerSwitcher(key: string, checked: boolean): void { if (!this.map || !this.view) return; this.activeBaseLayerKey = key; const oldLayer = this.layerRegistry.get(this.REGISTRY_KEY); if (oldLayer) { this.map.removeLayer(oldLayer); this.layerRegistry.delete(this.REGISTRY_KEY); } if (key == 's_province_boundaries') { this.addBaseDataLayer(this.baseLayerConfig, checked); this.hideBaseMapLayers(this.rasterBaseLayerKeys); } else { this.addBaseDataLayer(this.baseLayerConfig, checked, true); this.rasterBaseLayerKeys.forEach(layerKey => { if (layerKey === key) { this.addBaseDataLayer(servers.BaseLayer[layerKey], checked); } else { this.setBaseLayerVisible(layerKey, false); } }); } this.reapplyCurrentBaseMask(); } /** * 隐藏指定的底图图层 * @param layerKeys 要隐藏的图层 key 数组 */ hideBaseMapLayers(layerKeys: string[]): void { layerKeys.forEach(key => { const layer = this.layerRegistry.get(key); if (layer) { layer.setVisible(false); } }); } /** * 显示指定的底图图层 * @param layerKeys 要显示的图层 key 数组 */ showBaseMapLayers(layerKeys: string[]): void { layerKeys.forEach(key => { const layer = this.layerRegistry.get(key); if (layer) { layer.setVisible(true); } }); } /** * 添加梯级流域图 (Vector Layer) - 使用本地 GeoJSON 数据 * @param layer 图层配置对象 * @param fillcolor 填充颜色 * @param outlineColor 边框颜色 * @param datas 需要显示的 RVCD 列表 (例如: ['SJLY25', ...]),如果为空则显示所有或根据业务逻辑处理 */ addTertiarybasinLayer( layer: any, fillcolor: any, outlineColor: any, datas: any ): void { if (!this.map || !this.view) return; // 1. 检查是否已存在该图层,已存在时直接切换为显示并刷新样式 const existingLayer = this.layerRegistry.get(layer.key); // 2. 创建样式函数 const visibleStyle = new Style({ fill: new Fill({ color: fillcolor }), stroke: new Stroke({ color: outlineColor, width: 1 }) }); const hiddenStyle = new Style({ fill: new Fill({ color: 'rgba(0,0,0,0)' }), stroke: new Stroke({ color: 'rgba(0,0,0,0)' }) }); if (existingLayer) { const allowedIds = Array.isArray(datas) ? datas : []; const existingFeatures = existingLayer.getSource?.()?.getFeatures?.() ?? []; existingFeatures.forEach(feature => { const rvcd = feature.get('RVCD'); const isVisible = allowedIds.length > 0 ? rvcd && allowedIds.includes(rvcd) : true; feature.setStyle(isVisible ? visibleStyle : hiddenStyle); }); existingLayer.setVisible(layer.visible !== false); return; } // 3. 创建矢量源 (VectorSource),并通过 layer.geojson_url 加载数据 const vectorSource = new VectorSource(); if (layer?.geojson_url) { this.loadGeoJsonData(VITE_APP_MAP_URL + layer.geojson_url).then( geoJsonData => { if (!geoJsonData) return; const features = new GeoJSON().readFeatures(geoJsonData, { featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影 }); vectorSource.addFeatures(features); // 4. 数据过滤逻辑 const allowedIds = Array.isArray(datas) ? datas : []; features.forEach(feature => { // 获取属性中的 RVCD const rvcd = feature.get('RVCD'); let isVisible = false; // 逻辑:如果 datas 为空,通常意味着显示所有(或者都不显示,视具体需求而定) // 这里假设:datas 有值时严格过滤;datas 为空时显示所有 if (allowedIds.length > 0) { isVisible = rvcd && allowedIds.includes(rvcd); } else { isVisible = true; } // 应用样式 feature.setStyle(isVisible ? visibleStyle : hiddenStyle); }); } ); } // 5. 创建矢量图层 (VectorLayer) const vectorLayer = new VectorLayer({ source: vectorSource, style: null, // 样式已应用在 Feature 上,这里设为 null 或保留默认 zIndex: 100, visible: layer.visible !== false }); // 6. 注册并添加到地图 this.layerRegistry.set(layer.key, vectorLayer); this.map.addLayer(vectorLayer); } /** * 隐藏梯级流域图 * @param layer 图层配置对象 (需包含 key 属性) */ hideTertiarybasinLayer(layer: any): void { if (!this.map || !layer || !layer.key) { console.warn('hideTertiarybasinLayer: 无效的图层或地图未初始化'); return; } // 1. 从注册表中获取图层实例 const layerInstance = this.layerRegistry.get(layer.key); if (layerInstance) { layerInstance.setVisible(false); } else { console.warn(`未找到标识为 [${layer.key}] 的梯级流域图层实例`); } } /** * 删除描点图层 * @param layerKey 图层 key */ removePointLayer(layerKey: string): void { this.pointLayerManager.removeLayer(layerKey); } /** * 地图打印/导出 * 将当前地图视图导出为 PNG 图片,背景强制为白色 */ mapOutPut(): void { if (!this.map) { console.warn('地图未初始化,无法打印'); return; } const mapElement = this.map.getTargetElement(); const canvas = mapElement.querySelector('canvas'); if (!canvas) { console.error('未找到地图 Canvas 元素,无法导出'); return; } try { const width = canvas.width; const height = canvas.height; const offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = width; offscreenCanvas.height = height; const ctx = offscreenCanvas.getContext('2d'); if (!ctx) { throw new Error('无法获取 Canvas 上下文'); } ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, width, height); ctx.drawImage(canvas, 0, 0); const dataURL = offscreenCanvas.toDataURL('image/png'); const link = document.createElement('a'); link.download = `map_export_${new Date().getTime()}.png`; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (e) { console.error('地图导出失败', e); alert('地图导出失败,请检查控制台错误信息。'); } } /** * 切换双击缩放交互的状态 */ private toggleDoubleClickZoom(active: boolean): void { if (!this.map) return; this.map.getInteractions().forEach(interaction => { if (interaction instanceof DoubleClickZoom) { interaction.setActive(active); } }); } /** * 创建并返回测量 Tooltip Overlay */ private createMeasureTooltipOverlay(): Overlay { const element = document.createElement('div'); element.className = 'measure-tooltip tooltip-measure'; Object.assign(element.style, { background: 'rgba(0, 0, 0, 0.7)', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', whiteSpace: 'nowrap' }); return new Overlay({ element, offset: [0, -15], positioning: 'bottom-center' }); } /** * 通用测量启动方法 * @param geomType 'LineString' | 'Polygon' * @param drawStyle 绘制中的样式函数 * @param finishedStyle 完成后的样式 * @param onResult 计算结果的回调 (feature, resultText) */ private startMeasurement( geomType: 'LineString' | 'Polygon', drawStyle: any, finishedStyle: Style, onResult: (feature: Feature, text: string) => void ): void { if (!this.map || !this.view) return; // 1. 初始化图层 if (!this.measureLayer) { this.measureSource = new VectorSource(); this.measureLayer = new VectorLayer({ source: this.measureSource, zIndex: 1000 }); this.map.addLayer(this.measureLayer); } // 2. 清理旧交互 if (this.drawInteraction) { this.map.removeInteraction(this.drawInteraction); } // 禁用双击缩放 this.toggleDoubleClickZoom(false); // 3. 创建绘制交互 this.drawInteraction = new Draw({ source: this.measureSource!, type: geomType, style: drawStyle }); this.map.addInteraction(this.drawInteraction); let changeListenerKey: any; let currentTooltip: Overlay | null = null; // 4. 监听绘制开始 this.drawInteraction.on('drawstart', (evt: any) => { const feature = evt.feature; const geom = feature.getGeometry(); // 创建动态 Tooltip currentTooltip = this.createMeasureTooltipOverlay(); this.map?.addOverlay(currentTooltip); // 监听几何变化更新 Tooltip changeListenerKey = geom!.on('change', () => { if (currentTooltip && currentTooltip.getElement() && geom) { // 根据类型计算临时值 let tempText = ''; if (geomType === 'LineString') { tempText = this.formatLength(geom as LineString); } else { tempText = this.formatArea( geom as import('ol/geom/Polygon').default ); } currentTooltip.getElement()!.innerHTML = tempText; // 对于多边形,Tooltip 跟随鼠标最后一个点;对于线,也是最后一个点 const lastCoord = (geom as any).getLastCoordinate(); if (lastCoord) currentTooltip.setPosition(lastCoord); } }); }); // 5. 监听绘制结束 this.drawInteraction.on('drawend', (evt: any) => { const feature = evt.feature; const geom = feature.getGeometry(); // ✅ 修复:检查几何体是否有效 // 对于 Polygon,如果坐标点数不足(例如只点了两下),geom 可能是无效的或面积为0 if (!geom || geom.getCoordinates().length === 0) { console.warn('无效的几何体,已忽略'); this.measureSource?.removeFeature(feature); if (currentTooltip) this.map?.removeOverlay(currentTooltip); if (changeListenerKey) unByKey(changeListenerKey); return; } // 清理监听和临时 Tooltip if (changeListenerKey) unByKey(changeListenerKey); if (currentTooltip) this.map?.removeOverlay(currentTooltip); // ✅ 修复:强制设置样式,确保即使默认样式有问题也能显示 feature.setStyle(finishedStyle); // 计算最终结果并回调 let resultText = ''; let isValidResult = true; if (geomType === 'LineString') { resultText = this.formatLength(geom as LineString); } else { // 对于多边形,再次检查面积是否过小或无效 const polyGeom = geom as import('ol/geom/Polygon').default; const area = getSphericalArea(polyGeom); if (area < 0.0001) { // 极小面积视为无效绘制 isValidResult = false; console.warn('面积过小,已忽略'); this.measureSource?.removeFeature(feature); } else { resultText = this.formatArea(polyGeom); } } // 只有结果有效才添加标签 if (isValidResult) { onResult(feature, resultText); } // 清理交互 this.map?.removeInteraction(this.drawInteraction); this.drawInteraction = null; // 恢复双击缩放 setTimeout(() => { this.toggleDoubleClickZoom(true); }, 0); }); } /** * 长度量算 */ lengthCalculate(): void { this.startMeasurement( 'LineString', (feature: any) => { // 绘制中只显示线,隐藏点 return feature.getGeometry()?.getType() === 'LineString' ? new Style({ stroke: new Stroke({ color: '#ffcc33', lineDash: [10, 10], width: 2 }) }) : []; }, new Style({ stroke: new Stroke({ color: '#9AD8E7', width: 3 }) // 加粗一点 }), (feature, text) => { this.addFixedLabel(feature, text); } ); } /** * 面积量算 */ areCalculate(): void { this.startMeasurement( 'Polygon', (feature: any) => { // 绘制中显示线和填充 return feature.getGeometry()?.getType() === 'Polygon' ? new Style({ stroke: new Stroke({ color: '#ffcc33', lineDash: [10, 10], width: 2 }), fill: new Fill({ color: 'rgba(255, 204, 51, 0.3)' }) // 提高透明度以便看清 }) : []; }, new Style({ stroke: new Stroke({ color: '#9AD8E7', width: 3 }), // 加粗边框 fill: new Fill({ color: 'rgba(154, 216, 231, 0.4)' }) // 提高填充可见度 }), (feature, text) => { this.addFixedLabel(feature, text); } ); } /** * 移除量算结果 */ removeQueryLayer(): void { // 1. 清除矢量数据 (线条) if (this.measureSource) { this.measureSource.clear(); } // 2. 清除所有相关 Overlay (Label) if (this.map) { // ✅ 修复:先获取所有 Overlay 的副本,避免在遍历过程中修改原数组导致漏删 const overlaysToRemove: Overlay[] = []; this.map.getOverlays().forEach(overlay => { const el = overlay.getElement(); // 通过类名判断是否为我们创建的测量标签 if ( el && (el.classList.contains('measure-tooltip') || el.classList.contains('measure-label-container')) ) { overlaysToRemove.push(overlay); } }); // 统一移除 overlaysToRemove.forEach(overlay => { this.map?.removeOverlay(overlay); }); // 确保恢复双击缩放 this.toggleDoubleClickZoom(true); } // 3. 清除交互 if (this.drawInteraction) { this.map?.removeInteraction(this.drawInteraction); this.drawInteraction = null; } } /** * 添加固定的 Label (带删除按钮) */ private addFixedLabel(feature: Feature, lengthText: string): void { if (!this.map) return; const container = document.createElement('div'); container.className = 'measure-label-container'; // 使用 Object.assign 批量设置样式,代码更整洁 Object.assign(container.style, { position: 'absolute', backgroundColor: 'rgba(255, 255, 255, 0.9)', border: '1px solid #000', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', color: '#000', cursor: 'default', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', zIndex: '1001', boxShadow: '0 2px 4px rgba(0,0,0,0.2)', pointerEvents: 'auto' // 确保可以点击 }); const textSpan = document.createElement('span'); textSpan.innerText = lengthText; const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; Object.assign(closeBtn.style, { fontWeight: 'bold', marginLeft: '5px', cursor: 'pointer', fontSize: '14px', lineHeight: '1' }); const geom: any = feature.getGeometry(); let center: any; // ✅ 修复:根据几何类型选择标签位置 if (geom?.getType() === 'Polygon') { // 多边形使用内部点,确保标签在面内 center = (geom as import('ol/geom/Polygon').default) .getInteriorPoint() .getCoordinates(); } else { // 线使用中点 center = (geom as LineString).getCoordinateAt(0.5); } const overlay = new Overlay({ element: container, positioning: 'top-center', offset: [0, -10], position: center }); closeBtn.onclick = e => { e.stopPropagation(); this.measureSource?.removeFeature(feature); this.map?.removeOverlay(overlay); }; container.appendChild(textSpan); container.appendChild(closeBtn); this.map.addOverlay(overlay); } /** * 格式化长度输出 (米) */ private formatLength(line: LineString): string { return '长度:' + getSphericalLength(line).toFixed(3) + 'm'; } /** * 格式化面积输出 (km²) */ private formatArea(polygon: import('ol/geom/Polygon').default): string { // 获取球面面积 (平方米) const areaSqMeters = getSphericalArea(polygon); // 转换为平方公里 const areaSqKm = areaSqMeters / 1000000; return '面积:' + areaSqKm.toFixed(3) + 'km²'; } /** * 图例和基地面板控制描点数据显示隐藏方法 * @param layerType 图层类型 * @param key 图层标识符(锚点状态 nameEn) * @param baseid 基地ID * @param checked 是否选中(显示/隐藏) * @param isAll 是否全部操作 */ mdLayerShowOrHidden( layerType: string, key?: string, baseid: string, checked?: boolean, isAll?: boolean ): void { void baseid; void isAll; this.pointLayerManager.setLegendVisibleByField( layerType, key || '', !!checked ); } /** * 根据 anchoPointState 控制单个图例的锚点显示隐藏 * @param layerKey 图层 key * @param anchoPointState 锚点状态(对应图例的 nameEn) * @param checked 是否显示 */ setLegendPointVisible( layerKey: string, anchoPointState: string, checked: boolean ): void { this.pointLayerManager.setLegendVisibleByField( layerKey, anchoPointState, checked ); } switchView(_type): void {} fitBounds(): void {} /** * 飞行定位到指定经纬度和缩放级别 * @param position [经度, 纬度] (EPSG:4326) * @param zoom 缩放级别 */ flyTopanto(position: number[], zoom: number): void { if (!this.map || !this.view) { console.warn('地图未初始化,无法执行飞行定位'); return; } // 1. 将经纬度 [lon, lat] 转换为地图投影坐标 (EPSG:3857) const targetCenter = fromLonLat(position); // 2. 执行动画 // duration: 动画持续时间,单位毫秒,例如 1000ms (1秒) this.view.animate( { center: targetCenter, duration: 1000 }, { zoom: zoom, duration: 1000 } ); } /** * 生成带完整圆边框的上半圆 Canvas * @param radius 半径(像素) * @param color 填充颜色 * @returns HTMLCanvasElement */ private createSemiCircleCanvas( radius: number, color: string ): HTMLCanvasElement { const size = radius * 2 + 2; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d')!; ctx.clearRect(0, 0, size, size); const centerX = size / 2; const centerY = size / 2; // 1. 绘制下半圆填充(给定颜色)— 圆弧从 π 到 2π ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI, false); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); // 2. 绘制上半圆填充(白色) ctx.beginPath(); ctx.arc(centerX, centerY, radius, Math.PI, 2 * Math.PI, false); ctx.closePath(); ctx.fillStyle = '#FFFFFF'; // 白色 ctx.fill(); // 3. 绘制完整圆边框 ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; ctx.lineWidth = 1.5; ctx.stroke(); return canvas; } /** * 移除地图对象,释放资源 */ destroy(): void { this.removeQueryLayer(); this.regionMaskManager.destroy(); const clearLayer = (layer: any) => { if (layer && layer.getSource) { const source = layer.getSource(); if (source && source.clear) { source.clear(); } } if (this.map && this.map.getLayers().getArray().includes(layer)) { this.map.removeLayer(layer); } if (layer.setMap) layer.setMap(null); }; if (this.layerRegistry) { this.layerRegistry.forEach(clearLayer); this.layerRegistry.clear(); } this.popupManager.destroy(); this.pointLayerManager.destroy(); if (this.map) { this.map.getInteractions().clear(); this.map.getOverlays().clear(); this.map.setTarget(undefined); this.map.dispose(); this.map = null; } this.popupManager.setMap(null); this.pointLayerManager.setMap(null); this.regionMaskManager.setContext(null, null); if (this.view) { this.view.dispose(); this.view = null; } this.baseLayerConfig = null; this.measureSource = null; this.measureLayer = null; this.drawInteraction = null; this.geoJsonData = null; this.geoJsonData1 = null; } }