From 00a96f1275c27560fc733c9611d3a4987ffac187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=88=E5=85=86=E5=A2=9E?= <你的邮箱@example.com> Date: Mon, 20 Apr 2026 16:58:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BEol=20=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/gis/map.ol.ts | 1244 +++++++++++++++++++++++++ 1 file changed, 1244 insertions(+) create mode 100644 frontend/src/components/gis/map.ol.ts diff --git a/frontend/src/components/gis/map.ol.ts b/frontend/src/components/gis/map.ol.ts new file mode 100644 index 0000000..5b0d493 --- /dev/null +++ b/frontend/src/components/gis/map.ol.ts @@ -0,0 +1,1244 @@ +import { MapInterface } from './map'; +import OlMap from 'ol/Map'; +import View from 'ol/View'; +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 LayerGroup from 'ol/layer/Group'; +import OSM from 'ol/source/OSM'; +import WMTS from 'ol/source/WMTS'; +import WMTSTileGrid from 'ol/tilegrid/WMTS'; +import { get as getProjection, fromLonLat, toLonLat } 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 geoJsonData from '@/assets/geoJson.json'; +import geoJsonData1 from '@/assets/geoJson1.json'; +import { XYZ } from 'ol/source'; +import Feature from 'ol/Feature'; +import Point from 'ol/geom/Point'; // ✅ 新增导入 +import LineString from 'ol/geom/LineString'; +import Polygon from 'ol/geom/Polygon'; +import Overlay from 'ol/Overlay'; +import { + getLength as getSphericalLength, + getArea as getSphericalArea +} from 'ol/sphere'; +import { unByKey } from 'ol/Observable'; +import { MDOptions } from './map.class'; +import { getIconPath } from '@/utils/index'; // ✅ 确保引入获取图标路径的工具函数 + +// 定义与 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; + // ✅ 新增:存储 key -> layer 实例,用于控制显示隐藏和切换 + private layerRegistry: Map = new Map(); + // ✅ 新增:记录当前正在显示的“切换 Key”,用于判断是否需要重新加载或只是切换显隐 + private baseLayerConfig: TileLayer | null = null; + private hydropBaseConfig: TileLayer | null = null; + private REGISTRY_KEY = 'customBaseLayer'; + + // ✅ 新增:量算相关属性 + private drawInteraction: any | null = null; + private measureLayer: VectorLayer | null = null; + private measureSource: VectorSource | null = null; + // ✅ 新增:存储点图层的 Registry,key 为 layerType,value 为 VectorLayer + private pointLayerRegistry: Map = new Map(); + + private BASEID: string = '01'; + + //地图初始化 + init(container: HTMLElement, rectangle?: any): Promise { + try { + console.log('OL init初始化container', container); + + // 1. 将经纬度边界转换为 Web Mercator (EPSG:3857) 坐标,用于限制视图范围 + const minCoordinate = fromLonLat(BOUNDS_SW); + const maxCoordinate = fromLonLat(BOUNDS_NE); + const extent = [ + minCoordinate[0], + minCoordinate[1], + maxCoordinate[0], + maxCoordinate[1] + ]; + + // 2. 创建视图 (View) + this.view = new View({ + center: fromLonLat(CENTER_positionCN), // 设置中心点 + zoom: INITIAL_ZOOM, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, + constrainOnlyCenter: false, // 允许部分视图超出边界,但中心点受限(类似 Leaflet 默认行为) + extent: extent, // 限制视图范围 + smoothExtentConstraint: false // 平滑边界约束 + }); + + // ✅ 3. 自定义鼠标滚轮交互 + // Leaflet 的 wheelPxPerZoomLevel: 180 意味着缩放比较“慢/平滑”。 + // OpenLayers 默认每次滚动 delta 为 1/3 级。 + // 我们可以减小 delta 值来让缩放更平滑(需要更多滚动次数才能变一级), + // 或者使用 constrainResolution: false 允许非整数缩放,获得更丝滑的效果。 + const mouseWheelInteraction = new MouseWheelZoom({ + duration: 100, // 缩放动画持续时间 (ms),Leaflet 默认也有动画 + maxDelta: 0.5, + constrainResolution: false // ✅ 关键:false 允许缩放到非整数级别 (如 4.5, 4.6),实现平滑缩放 + }); + + // 4. 创建地图实例 + 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]) + }); + + // 5. 监听缩放结束事件 (对应 Leaflet 的 'zoomend') + this.view.on('change:resolution', () => { + // OpenLayers 没有直接的 'zoomend' 事件,通常监听 resolution 变化 + // 如果需要防抖处理,可以添加 debounce + console.log('当前缩放级别', this.view?.getZoom()); + }); + + // ✅ 新增:监听鼠标移动,动态改变光标样式 + this.map.on('pointermove', evt => { + const pixel = evt.pixel; + // 检测鼠标下方是否有矢量要素 + const feature = this.map?.forEachFeatureAtPixel(pixel, feature => { + return feature; + }); + + const targetElement = this.map?.getTargetElement() as HTMLElement; + + if (targetElement) { + if (feature) { + // 如果鼠标下有要素,显示手型 + targetElement.style.cursor = 'pointer'; + } else { + // 否则恢复默认 + targetElement.style.cursor = ''; + } + } + }); + return Promise.resolve(this.map); + } catch (e) { + console.log('OL Init Error', e); + return Promise.reject(e); + } + } + + /** + * 初始化加载描点数据 + * @param pointData 图层配置对象或数据数组 + * @param layerType 图层类型/Key + * @param mdoptions 描点选项配置 + */ + addInitDataLayer( + pointData: any, + layerType: any, + mdoptions?: MDOptions + ): void { + if (!this.map) return; + + let dataArray: any[] = []; + let targetLayerKey: string = layerType; + + // 1. 参数解析 + if (Array.isArray(pointData)) { + dataArray = pointData; + } else { + dataArray = pointData.data || []; + targetLayerKey = pointData.key || layerType; + } + + if (!targetLayerKey) { + console.warn('缺少图层 Key,无法加载描点'); + return; + } + + // 2. 获取或创建 VectorLayer + let vectorLayer = this.pointLayerRegistry.get(targetLayerKey); + let vectorSource: VectorSource; + + const shouldClear = mdoptions?.isRemove !== false; + + if (!vectorLayer) { + vectorSource = new VectorSource(); + vectorLayer = new VectorLayer({ + source: vectorSource, + zIndex: 100, // 确保点在底图之上 + declutter: true, + style: (feature: any) => this.createPointStyle(feature) // 使用动态样式函数 + }); + this.pointLayerRegistry.set(targetLayerKey, vectorLayer); + this.map.addLayer(vectorLayer); + } else { + vectorSource = vectorLayer.getSource() as VectorSource; + if (shouldClear) { + vectorSource.clear(); + } + } + + if (dataArray.length === 0) { + return; + } + + console.log( + `开始加载 OL 图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}` + ); + + // 3. 遍历数据生成 Feature + const features: Feature[] = []; + + dataArray.forEach((item: any) => { + const { + lgtd, + lttd, + stcd, + stnm, + iconCode, + anchoPointState, + titleName, + ennm + } = item; + + if (lgtd == null || lttd == null) { + return; + } + + // 4. 确定图标和文字 + let iconUrl = ''; + + // 获取图标 URL (参考 Leaflet 逻辑) + if (iconCode) { + iconUrl = getIconPath(iconCode); + } + // 如果没有 iconCode,可以根据 anchoPointState 或其他逻辑获取,这里暂略 + + if (!iconUrl) { + // 默认图标,防止报错 + iconUrl = getIconPath('default') || ''; + } + + // 显示的文字优先使用 titleName,其次 ennm,最后 stnm + const labelText = titleName || ennm || stnm || ''; + + // 5. 创建 Feature + const feature = new Feature({ + geometry: new Point(fromLonLat([lgtd, lttd])), // ✅ 关键:坐标转换 EPSG:4326 -> EPSG:3857 + ...item, // 保留原始数据以便点击事件使用 + _layerKey: targetLayerKey + }); + + // 将自定义属性绑定到 feature,以便样式函数访问 + feature.set('_iconUrl', iconUrl); + feature.set('_labelText', labelText); + + features.push(feature); + }); + + // 6. 批量添加要素 + if (features.length > 0) { + vectorSource.addFeatures(features); + } + + console.log(`图层 [${targetLayerKey}] 描点加载完成`); + } + + /** + * 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小) + */ + private createPointStyle(feature: Feature): Style { + const iconUrl = feature.get('_iconUrl') as string; + const labelText = feature.get('_labelText') as string; + + // 如果没图标,返回空样式 + if (!iconUrl) { + return new Style(); + } + + // ✅ 1. 获取当前地图分辨率 + // 注意:如果样式函数在地图未完全初始化时调用,view 可能为 null + const currentResolution = this.view ? this.view.getResolution() : 1000; + + // ✅ 2. 定义基准分辨率和基准缩放比例 + // INITIAL_ZOOM (4.5) 对应的分辨率大约是 3000-4000 左右 (取决于投影) + // 这里我们用一个经验公式:分辨率越小(zoom越大),scale 越大 + // 假设在初始分辨率下 scale 为 0.7 + const baseResolution = 3000; // 这是一个估算值,你可以根据实际效果微调 + const baseScale = 0.7; + + // 计算动态缩放比例: + // 如果 currentResolution 变小 (放大地图),ratio 变大 -> icon 变大 + // 限制最大和最小缩放,防止过大或过小 + // let dynamicScale = baseScale * (baseResolution / (currentResolution || 1)); + const currentZoom = this.view ? this.view.getZoom() : 4.5; + let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08; + + // 限制范围:最小 0.5,最大 3.0 (可根据需求调整) + dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale)); + + // ✅ 3. 动态字体大小 + // 基础字体 12px,随缩放比例线性变化 + const fontSize = Math.max(10, Math.min(24, 12 * dynamicScale)); + + return new Style({ + image: new Icon({ + src: iconUrl, + scale: dynamicScale, // ✅ 使用动态缩放 + anchor: [0.5, 0.5], // 锚点:水平居中,底部对齐 + // 确保图标清晰 + crossOrigin: 'anonymous' + }), + 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' + }) + }); + } + /** + * 初始化加载基础图层 + * @param layer 图层配置对象 + */ + addBaseDataLayer(layer: any, isCache: boolean = false): void { + if (!this.map) return; + + console.log('OL addBaseDataLayer', layer); + + 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; + } + // 1. 解析参数 + // 注意:如果 layer.url 已经是完整 URL,可能需要调整解析逻辑。 + // 这里假设 layer.url 包含查询参数,或者我们直接使用硬编码的配置来确保正确性 + const layerName: string = urlParams.get('LAYER') || ''; // 根据你的 Leaflet 代码硬编码或从 layer 对象获取 + const matrixSetName: any = urlParams.get('TILEMATRIXSET'); // TileMatrixSet 名称 + + // 2. 获取标准 EPSG:3857 投影信息 + const projection = getProjection('EPSG:3857'); + if (!projection) { + console.error('无法获取 EPSG:3857 投影'); + return; + } + const projectionExtent = projection.getExtent(); + + // 3. 计算分辨率和矩阵ID + // GeoServer 的 TileMatrixSet 通常基于标准 Web Mercator,但 ID 格式可能不同 + const size = getWidth(projectionExtent) / 256; + const maxZoom = 13; // 根据 Leaflet 中的 maxNativeZoom: 12 或 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); + + // 【关键修复】:构造符合 GeoServer 要求的 Matrix ID + // 通常格式为: "TileMatrixSetName:ZoomLevel" 或 "EPSG:3857:ZoomLevel" + // 根据你的 URL 推测,ID 可能是 "EPSG:3857_qgc_qsj_arcgistiles_l13:0" + matrixIds[z] = `${matrixSetName}:${z}`; + } + + console.log('Using Matrix IDs sample:', matrixIds[0], matrixIds[1]); + + // 4. 创建 WMTS 图层 + const wmtsLayer = new TileLayer({ + source: new WMTS({ + url: layer.url.split('?')[0], // 使用代理路径 + layer: layerName, + matrixSet: matrixSetName, + format: 'image/png', + projection: projection, // ✅ 使用标准的 EPSG:3857 投影对象 + tileGrid: new WMTSTileGrid({ + origin: getTopLeft(projectionExtent), // ✅ 使用标准投影的左上角原点 + resolutions: resolutions, + matrixIds: matrixIds // ✅ 使用上面生成的带前缀的 IDs + }), + style: 'default', + wrapX: true, // 允许水平方向上的循环 + crossOrigin: 'anonymous' // 允许跨域 + }) + }); + + if (layer.key === this.REGISTRY_KEY) { + !isCache ? wmtsLayer.setZIndex(-100) : wmtsLayer.setZIndex(-99); + } else { + wmtsLayer.setZIndex(-99); + } + // 5. 注册与添加 + 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); + this.layerRegistry.set(this.REGISTRY_KEY, tileLayer); + this.map.addLayer(tileLayer); + } else if (layer.type === 'vector') { + if (layer.key === 'hydropBase') { + this.hydropBaseConfig = layer; + } + // ✅ 1. 创建矢量源,关键是要配置投影转换 + const vectorSource = new VectorSource({ + features: new GeoJSON().readFeatures(geoJsonData1, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影 + }) + // url: layer.geojson_url +`&BASEID=BASEID'${this.BASEID}'` , // 远程 GeoJSON 地址 + // format: new GeoJSON({ + // featureProjection: 'EPSG:3857' + // }) + }); + + // ✅ 2. 创建矢量图层,添加默认样式防止透明 + const vectorLayer = new VectorLayer({ + source: vectorSource, + style: new Style({ + stroke: new Stroke({ + color: '#3399CC', // 默认蓝色边框 + width: 2 + }), + fill: new Fill({ + color: 'rgba(51, 153, 204, 0.4)' // 默认半透明填充 + }) + }), + // ✅ 3. 调整 zIndex,确保在底图之上可见 + zIndex: 100, + visible: true + }); + // 监听数据加载完成事件 + // vectorSource.on('featuresloadend', () => { + // // 获取所有加载的要素 + // const features = vectorSource.getFeatures(); + // const feature = features[0]; + // const geometry = feature.getGeometry(); + // console.log(geometry) + // // this.addClipToRasterLayer(this.layerRegistry.get(this.REGISTRY_KEY), geometry); + // // this.applyMapMask(geometry); + // }); + // 4. 注册与添加 + this.layerRegistry.set(layer.key, vectorLayer); + this.map.addLayer(vectorLayer); + layer._layer = vectorLayer; + + console.log(`矢量图层 [${layer.key}] 已加载: ${layer.url}`); + } + } + /** + * 为栅格图层添加裁切效果 + * @param layer 栅格图层(TileLayer) + * @param clipGeometry 裁切几何(Polygon 或 MultiPolygon,EPSG:3857 投影) + */ +private addClipToRasterLayer(layer: TileLayer, clipGeometry: Geometry): void { + if (!layer || !clipGeometry) return; + + // 处理几何类型,统一为多边形数组 + let polygons: Polygon[] = []; + const type = clipGeometry.getType(); + + if (type === 'Polygon') { + polygons = [clipGeometry as Polygon]; + } else if (type === 'MultiPolygon') { + const multiPolygon = clipGeometry as MultiPolygon; + const coords = multiPolygon.getCoordinates(); + polygons = coords.map(coord => new Polygon(coord)); + } else { + console.error('不支持的几何类型:', type); + return; + } + + // 预存储所有外环坐标 + const allRings: number[][][] = []; + for (const polygon of polygons) { + const coords = polygon.getCoordinates(); + if (coords && coords[0] && coords[0].length > 0) { + allRings.push(coords[0]); + } + } + + // 移除旧事件 + layer.removeEventListener('prerender'); + layer.removeEventListener('postrender'); + + // 渲染前:设置裁切区域 + layer.on('prerender', (event) => { + const context = event.context; + const frameState = event.frameState; + + if (!context || !frameState) return; + + // 获取坐标转像素函数 + const toPixel = frameState.coordinateToPixel; + if (!toPixel) return; + + context.save(); + context.beginPath(); + + let hasPath = false; + + for (const ring of allRings) { + if (!ring || ring.length === 0) continue; + + for (let i = 0; i < ring.length; i++) { + const coord = ring[i]; + const pixel = toPixel(coord); + + if (!pixel || pixel.length < 2) continue; + + if (i === 0) { + context.moveTo(pixel[0], pixel[1]); + } else { + context.lineTo(pixel[0], pixel[1]); + } + hasPath = true; + } + context.closePath(); + } + + if (hasPath) { + context.clip(); + } else { + context.restore(); + } + }); + + // 渲染后:恢复状态 + layer.on('postrender', (event) => { + if (event.context) { + event.context.restore(); + } + }); +} + /** + * 控制特定区域图层的显示(互斥显示) + * @param regionId 区域ID (例如 "hebei", "13", "01" 等,需与图层 Key 或属性对应) + * @param isAll 是否显示所有 (true: 显示所有图层; false: 仅显示匹配 regionId 的图层,隐藏其他) + */ + jdPanelControlShowAndHidden(regionId: string, isAll: boolean): void { + this.BASEID = regionId; + console.log(this.layerRegistry); + console.log(this.layerRegistry.keys()); + // this.addBaseDataLayer(this.hydropBaseConfig); + // this.controlBaseLayerTreeShowAndHidden(this.REGISTRY_KEY,this.REGISTRY_KEY,false) + } + /** + * 基础图层显示影隐藏方法 + * @param layerType 图层类型 (备用,优先使用 key) + * @param key 图层唯一标识 Key + * @param checked true 为显示,false 为隐藏 + */ + controlBaseLayerTreeShowAndHidden( + layerType: String, + key: String, + checked: boolean + ): void { + if (!this.map) return; + + // 1. 确定查找的 Key:优先使用传入的 key,如果为空则尝试使用 layerType + const registryKey = key || layerType; + + if (!registryKey) { + console.warn( + 'controlBaseLayerTreeShowAndHidden: 缺少有效的 Key 或 LayerType' + ); + return; + } + // 2. 从注册表中获取图层实例 + const layerInstance = this.layerRegistry.get(registryKey as string); + + if (layerInstance) { + // 3. 设置可见性 + // OpenLayers 的 Layer.setVisible(boolean) 方法 + layerInstance.setVisible(checked); + + console.log( + `图层 [${registryKey}] 已设置为: ${checked ? '显示' : '隐藏'}` + ); + } else { + console.warn( + `未找到标识为 [${registryKey}] 的图层实例,当前注册表 keys:`, + Array.from(this.layerRegistry.keys()) + ); + } + } + // 图层树控制描点数据显示隐藏方法 + mdLayerTreeShowOrHidden(layerType: String, checked?: boolean): void { + const layerInstance = this.pointLayerRegistry.get(layerType as string); + layerInstance.setVisible(checked); + // this.service.mdLayerTreeShowOrHidden(layerType, checked) + } + /** + * 缩放地图 + * @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): void { + if (!this.map || !this.view) return; + + console.log('OL 切换底图 key:', key); + + // 1. 如果注册表中已存在 "customBaseLayer",先将其从地图中移除 + const oldLayer = this.layerRegistry.get(this.REGISTRY_KEY); + if (oldLayer) { + this.map.removeLayer(oldLayer); + this.layerRegistry.delete(this.REGISTRY_KEY); + } + + // 2. 根据传入的业务 key 创建新的图层实例 + if (key == 's_province_boundaries') { + this.addBaseDataLayer(this.baseLayerConfig); + } else { + this.addBaseDataLayer(this.baseLayerConfig, true); + this.addBaseDataLayer(servers.BaseLayer[key]); + } + } + /** + * 添加梯级流域图 (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); + if (existingLayer) { + this.map.removeLayer(existingLayer); + this.layerRegistry.delete(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)' }) + }); + + // 3. 创建矢量源 (VectorSource) 并直接加载数据 + const vectorSource = new VectorSource({ + features: new GeoJSON().readFeatures(geoJsonData, { + featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影 + }) + }); + + // 4. 数据过滤逻辑 + const allowedIds = Array.isArray(datas) ? datas : []; + const features = vectorSource.getFeatures(); + + 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 属性) + */ + removeTertiarybasinLayer(layer: any): void { + if (!this.map || !layer || !layer.key) { + console.warn('removeTertiarybasinLayer: 无效的图层或地图未初始化'); + return; + } + // 1. 从注册表中获取图层实例 + const layerInstance = this.layerRegistry.get(layer.key); + + if (layerInstance) { + // 2. 从地图中移除图层 + this.map.removeLayer(layerInstance); + // 3. 从注册表中删除引用 + this.layerRegistry.delete(layer.key); + + console.log(`梯级流域图层 [${layer.key}] 已移除`); + } else { + console.warn(`未找到标识为 [${layer.key}] 的梯级流域图层实例`); + } + } + /** + * 地图打印/导出 + * 将当前地图视图导出为 PNG 图片,背景强制为白色 + */ + mapOutPut(): void { + if (!this.map) { + console.warn('地图未初始化,无法打印'); + return; + } + + console.log('开始生成地图打印图片...'); + + // 1. 获取地图容器中的 canvas 元素 + const mapElement = this.map.getTargetElement(); + const canvas = mapElement.querySelector('canvas'); + + if (!canvas) { + console.error('未找到地图 Canvas 元素,无法导出'); + return; + } + + try { + // 2. 创建一个离屏 Canvas 用于合成白色背景 + 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 上下文'); + } + + // 3. 填充白色背景 + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(0, 0, width, height); + + // 4. 将原地图 Canvas 绘制到离屏 Canvas 上 + ctx.drawImage(canvas, 0, 0); + + // 5. 将合成后的 Canvas 转换为 Data URL + const dataURL = offscreenCanvas.toDataURL('image/png'); + + // 6. 创建临时链接并触发下载 + 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); + + console.log('地图图片已生成并触发下载(背景已白化)'); + } 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; + + console.log(`启动${geomType === 'LineString' ? '长度' : '面积'}量算模式`); + + // 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); + + console.log('测量结束'); + }); + } + /** + * 长度量算 + */ + 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; + } + + console.log('已清除所有量算结果'); + } + + /** + * 添加固定的 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²'; + } + /** + * 移除地图对象,释放资源 + */ + destroy(): void { + console.log('销毁地图实例...'); + + // 1. 清除量算相关资源 + this.removeQueryLayer(); + + // 2. 清除所有通过 layerRegistry 管理的图层 + if (this.layerRegistry) { + this.layerRegistry.forEach((layer, key) => { + if (this.map && this.map.getLayers().getArray().includes(layer)) { + this.map.removeLayer(layer); + } + }); + this.layerRegistry.clear(); + } + + // ✅ 3. 清除点图层 Registry + if (this.pointLayerRegistry) { + this.pointLayerRegistry.forEach((layer, key) => { + if (this.map && this.map.getLayers().getArray().includes(layer)) { + this.map.removeLayer(layer); + } + }); + this.pointLayerRegistry.clear(); + } + + // 4. 清除地图实例 + if (this.map) { + this.map.getInteractions().clear(); + this.map.getOverlays().clear(); + this.map.setTarget(null); + this.map.dispose(); + this.map = null; + } + + // 5. 清除视图引用 + if (this.view) { + this.view.dispose(); + this.view = null; + } + + // 6. 清除其他引用 + this.baseLayerConfig = null; + this.measureSource = null; + this.measureLayer = null; + this.drawInteraction = null; + + console.log('地图实例已销毁'); + } + // ... 其他 MapInterface 方法待实现 +}