WholeProcessPlatform/frontend/src/components/gis/map.ol.ts

1752 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Circle from 'ol/style/Circle';
import RegularShape from 'ol/style/RegularShape';
// 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 } 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 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<string, any> = new Map();
// ✅ 新增:记录当前正在显示的“切换 Key”用于判断是否需要重新加载或只是切换显隐
// 基础图层配置
private baseLayerConfig: TileLayer | null = null;
// 基地配置
private hydropBaseConfig: any | null = null;
private REGISTRY_KEY = 'customBaseLayer';
// ✅ 新增:量算相关属性
private drawInteraction: any = null;
private measureLayer: VectorLayer | null = null;
private measureSource: VectorSource | null = null;
// ✅ 新增:存储点图层的 Registrykey 为 layerTypevalue 为 VectorLayer
private pointLayerRegistry: Map<string, VectorLayer> = new Map();
geoJsonData: any = null;
geoJsonData1: any = null;
// ✅ 新增:声明用于存储 mask 事件监听器的引用,以便后续移除
private _maskPrerender: any = null;
private _maskPostrender: any = null;
private BASEID: string = '';
// ✅ 新增:记录当前鼠标悬停的 Feature ID
private hoveredFeatureId: string | number | null = null;
// ✅ 新增:用于显示自定义内容的 Overlay
private popupOverlay: Overlay | null = null;
private popupElement: HTMLElement | null = null;
constructor() {}
private async loadGeoJsonData(url): Promise<any> {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('配置加载失败:', error);
}
}
//地图初始化
init(container: HTMLElement): Promise<any> {
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: 2,
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());
});
// ✅ 新增:监听鼠标移动,动态改变光标样式
let lastHoveredId: string | number | null = null;
let animationFrameId: number | null = null;
// pointermove
this.map.on('click', evt => {
const pixel = evt.pixel;
// 取消之前的动画帧,确保只处理最新的一次移动
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
animationFrameId = requestAnimationFrame(() => {
let detectedFeature: Feature | undefined = undefined;
let isHitIcon: boolean = false; // 标记是否真正命中了图标区域
// 1. 获取当前视口下的所有点图层
const pointLayers = Array.from(this.pointLayerRegistry.values());
// 2. 遍历检测,但我们需要更精细的判断
this.map?.forEachFeatureAtPixel(
pixel,
(feature, layer) => {
// 只关心点图层
if (pointLayers.includes(layer)) {
detectedFeature = feature as Feature;
// ✅ 关键优化:判断是否命中图标区域
// OpenLayers 的 Icon 样式通常有 anchor [0.5, 0.5]
// 我们可以获取图标的尺寸,判断鼠标是否在图标矩形范围内
const style = (layer as VectorLayer).getStyle();
let iconSize = { width: 0, height: 0 };
// 尝试从样式中获取图标尺寸(如果样式是函数,可能需要调用它,这里简化处理,假设固定大小或从 feature 获取)
// 更通用的做法是:计算鼠标点到 Feature 坐标投影后的像素距离
const geom = feature.getGeometry();
if (geom && geom.getType() === 'Point') {
const coord = (geom as Point).getCoordinates();
const iconPixel = this.map?.getPixelFromCoordinate(coord);
if (iconPixel) {
// 计算鼠标像素与图标中心像素的距离
const dx = pixel[0] - iconPixel[0];
const dy = pixel[1] - iconPixel[1];
const distance = Math.sqrt(dx * dx + dy * dy);
// 假设图标大致大小为 30x30 或 40x40半径设为 15-20
// 你可以根据实际 icon 大小调整这个阈值,例如 15
const hitRadius = 15;
if (distance <= hitRadius) {
isHitIcon = true;
}
}
}
return true; // 找到第一个点图层要素即停止
}
return false;
},
{ hitTolerance: 0 } // 设置为 0完全依赖上面的距离计算
);
// 3. 根据是否命中图标来决定行为
const newHoveredId =
detectedFeature && isHitIcon ? detectedFeature.getId() : null;
const detectedCoordinate =
detectedFeature && isHitIcon
? detectedFeature.getGeometry()?.getCoordinates()
: undefined;
// ✅ 关键优化:立即更新状态,不再使用 setTimeout 防抖,以解决移出延时问题
// 如果希望防止极速移动时的闪烁,可以保留极短的 debounce (如 10ms),但通常 requestAnimationFrame 已足够平滑
if (newHoveredId !== lastHoveredId) {
lastHoveredId = newHoveredId;
this.hoveredFeatureId = newHoveredId;
// 更新光标
const targetElement = this.map?.getTargetElement() as HTMLElement;
if (targetElement) {
targetElement.style.cursor = newHoveredId ? 'pointer' : '';
}
// 显示/隐藏 Popup
this.showPopup(detectedFeature, detectedCoordinate);
// 重绘图层以更新图标样式(如高亮)
this.pointLayerRegistry.forEach(layer => {
layer.changed();
});
// 触发地图渲染
this.map?.render();
}
});
});
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) // 使用动态样式函数
style: (feature: any) => this.createPointStyle1(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) => {
let { lgtd, lttd, stnm, iconCode, 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
});
const uniqueId = item.stcd || item._id;
feature.setId(uniqueId);
// 将自定义属性绑定到 feature以便样式函数访问
feature.set('_iconUrl', iconUrl);
feature.set('_labelText', labelText);
if (item.popupHtml) {
feature.set('popupHtml', item.popupHtml);
}
features.push(feature);
});
// 6. 批量添加要素
if (features.length > 0) {
vectorSource.addFeatures(features);
}
console.log(`图层 [${targetLayerKey}] 描点加载完成`);
}
/**
* 初始化 Popup Overlay
* @param container 父组件传入的 DOM 容器
*/
initPopupOverlay(container: HTMLElement) {
this.popupElement = container;
this.popupOverlay = new Overlay({
element: this.popupElement,
positioning: 'bottom-center', // 气泡在点位上方
offset: [0, -10], // 向上偏移一点
stopEvent: false, // 允许点击事件穿透到地图(如果需要点击地图关闭)
autoPan: false
// autoPan: {
// animation: {
// duration: 250
// }
// }
});
if (this.map) {
this.map.addOverlay(this.popupOverlay);
}
}
/**
* 在 pointermove 中调用此方法显示/隐藏 Popup
* @param feature 当前悬停的要素
* @param coordinate 地理坐标
*/
showPopup(feature: Feature | undefined, coordinate: number[] | undefined) {
if (!this.popupOverlay || !this.popupElement) return;
if (feature && coordinate) {
// 1. 获取数据
const props = feature.getProperties();
// 2. 生成内容 (这里假设父组件已经在 feature 上绑定了 popupHtml 或者我们在这里构建)
// 方案 A: 如果父组件传入了 HTML 字符串
const popupHtml = props.popupHtml || props.popup || '';
// 方案 B: 如果父组件希望动态更新 DOM (推荐,性能更好,支持交互)
// 我们可以触发一个事件,让 Vue 组件去更新这个 div 的内容
// 这里为了简单,直接演示如何填充内容
if (popupHtml) {
this.popupElement.innerHTML = popupHtml;
this.popupOverlay.setPosition(coordinate);
this.popupElement.style.display = 'block';
} else {
// 如果没有预设 HTML可以显示默认文本
this.popupElement.innerHTML = `<div style="padding:5px; background:white; border-radius:4px;">${
props.stnm || props.titleName || '未知'
}</div>`;
this.popupOverlay.setPosition(coordinate);
this.popupElement.style.display = 'block';
}
} else {
// 隐藏
this.popupOverlay.setPosition(undefined);
this.popupElement.style.display = 'none';
}
}
/**
* 创建点样式 (模拟 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: any = 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));
const currentFeatureId = feature.getId() || (feature as any).ol_uid;
const isHovered = this.hoveredFeatureId === currentFeatureId;
const zIndex = isHovered ? 100 : 0;
const styleOptions: any = {
image: new Icon({
src: iconUrl,
scale: dynamicScale,
anchor: [0.5, 0.5],
crossOrigin: 'anonymous'
})
// zIndex: zIndex // ✅ 设置样式的 zIndex
};
// ✅ 4. 只有在悬停时才添加 Text 样式
// if (isHovered && labelText) {
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 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));
// const fontSize = Math.max(46, Math.min(24, 12 * dynamicScale));
// const radius = Math.max(14, Math.min(18, 6 * dynamicScale));
const currentFeatureId = feature.getId() || (feature as any).ol_uid;
const isHovered = this.hoveredFeatureId === currentFeatureId;
// 根据 BuildType 选择图形样式
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
};
// 悬停时添加文本标签(与原逻辑相同,可根据半圆形状微调 offsetY
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, 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}`;
}
// 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);
}
console.log('基础图层', wmtsLayer);
// 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;
}
console.log(`矢量图层 [${layer.key}] 已加载: ${layer.url}`);
}
}
// 在 addBaseDataLayer 方法的 vector 分支末尾,或者专门提供一个方法来激活遮罩
enableNortheastMask(): void {
// 1. 获取全量底图图层
const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY);
// 2. 检查是否已加载东北边界数据
if (!this.geoJsonData1) {
console.warn('东北边界数据尚未加载');
return;
}
if (baseLayer && baseLayer instanceof TileLayer) {
console.log('正在应用东北区域遮罩...');
this.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<void> {
if (!this.map || !this.hydropBaseConfig) {
console.warn('地图未初始化或 hydropBaseConfig 未配置');
return;
}
// 1. 更新当前选中的区域 ID
this.BASEID = _regionId;
console.log(`切换区域至: ${this.BASEID}`);
if (!isAll) {
// ✅ 情况 A: isAll == false -> 清空裁切
this.clearMapMask();
return;
}
// 2. 获取底图图层 (用于应用遮罩的目标)
const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY) as TileLayer;
if (!baseLayer) {
console.warn('未找到底图图层,无法应用遮罩');
return;
}
try {
const url =
this.hydropBaseConfig.geojson_url +
`&cql_filter=BASEID='${this.BASEID}'`;
console.log('正在请求裁切数据:', url);
// 等待数据加载
const geoJsonData = await this.loadGeoJsonData(url);
console.log('获取到的原始 GeoJSON:', geoJsonData);
// ✅ 新增:在应用遮罩前,先让地图视角聚焦到该区域
this.fitViewToGeoJson(geoJsonData);
console.log('基础图层', baseLayer);
// 应用遮罩
this.applyMapMask(baseLayer, geoJsonData);
} catch (error) {
console.error('加载裁切数据失败:', error);
// 出错时也清空遮罩,保证用户体验
this.clearMapMask();
}
}
/**
* 基础图层显示影隐藏方法
* @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: any = 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(this.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 = '&times;';
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);
}
/**
* 为栅格图层添加基于矢量的裁切效果
* @param rasterLayer 需要被裁切的底图图层 (TileLayer)
* @param clipGeoJson GeoJSON 格式的裁切边界数据
*/
private applyMapMask(rasterLayer: TileLayer, clipGeoJson: any): void {
if (!rasterLayer || !clipGeoJson) return;
// 1. 解析 GeoJSON 为 OL Features (EPSG:3857)
const features = new GeoJSON().readFeatures(clipGeoJson, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857'
});
if (!features || features.length === 0) {
console.warn('裁切数据为空,无法应用遮罩');
return;
}
// 2. 移除旧的监听器,防止重复绑定
if (this._maskPrerender) {
rasterLayer.un('prerender', this._maskPrerender);
}
if (this._maskPostrender) {
rasterLayer.un('postrender', this._maskPostrender);
}
// 3. 定义 prerender 事件处理函数
// OpenLayers 在 prerender 时context 已经包含了当前帧的状态
const maskPrerender = (event: any) => {
const context = event.context;
const frameState = event.frameState;
if (!context || !frameState || !this.map) return;
// ✅ 关键步骤 1: 保存当前 Canvas 状态
context.save();
// ✅ 关键步骤 2: 重置变换矩阵,确保我们操作的是屏幕像素坐标
// OpenLayers 默认可能应用了平移/缩放,我们需要手动控制
context.setTransform(1, 0, 0, 1, 0, 0);
context.beginPath();
let hasValidPath = false;
// 遍历所有特征,绘制路径
for (const feature of features) {
const geom = feature.getGeometry();
if (!geom) continue;
const type = geom.getType();
// 处理 Polygon
if (type === 'Polygon') {
const rings = (geom as any).getCoordinates() as number[][][];
for (const ring of rings) {
if (this.drawRingToContext(context, ring, frameState)) {
hasValidPath = true;
}
}
}
// 处理 MultiPolygon
else if (type === 'MultiPolygon') {
const polygons = (geom as any).getCoordinates() as number[][][][];
for (const polygonRings of polygons) {
for (const ring of polygonRings) {
if (this.drawRingToContext(context, ring, frameState)) {
hasValidPath = true;
}
}
}
}
}
// ✅ 关键步骤 3: 如果绘制了有效路径,则执行裁剪
if (hasValidPath) {
// 闭合路径(虽然 drawRing 里做了 closePath但为了保险
context.save();
// 先用外描边创建裁剪区域
context.lineWidth = 6;
context.strokeStyle = '#6D64DF';
context.stroke();
context.clip(); // 裁剪到外描边的内部区域
context.strokeStyle = '#CCC9F4';
context.lineWidth = 5;
context.stroke();
// 执行裁剪!此后所有绘制操作仅在此路径内可见
context.clip();
// 可选:如果你希望裁剪区域外有半透明黑色遮罩,可以在这里绘制一个全屏矩形
// 但通常我们只希望“隐藏”外部,所以只需 clip 即可OL 会自动绘制瓦片
} else {
console.warn('未能生成有效的裁切路径');
}
};
// 4. 定义 postrender 事件处理函数
// 用于恢复 Canvas 状态,避免影响其他图层或后续帧
const maskPostrender = (event: any) => {
if (event.context) {
event.context.restore();
}
};
// 5. 绑定新的事件监听器
this._maskPrerender = maskPrerender;
this._maskPostrender = maskPostrender;
rasterLayer.on('prerender', this._maskPrerender);
rasterLayer.on('postrender', this._maskPostrender);
// 6. 强制触发重绘
rasterLayer.changed();
console.log('地图遮罩已应用');
}
/**
* 将单个环Ring绘制到 Canvas 上下文
* @returns boolean 是否成功绘制了有效路径
*/
private drawRingToContext(
context: CanvasRenderingContext2D,
ring: number[][],
frameState: any
): boolean {
if (!ring || ring.length < 3) return false; // 至少需要3个点构成面
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;
// 获取 CSS 像素坐标
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;
}
/**
* 根据 GeoJSON 数据调整地图视野,使其居中并适配大小
* @param geoJson GeoJSON 对象
*/
private fitViewToGeoJson(geoJson: any): void {
if (!this.map || !this.view) return;
try {
// 1. 将 GeoJSON 转换为 OpenLayers 的 Feature 数组
// 注意:这里假设输入的是 EPSG:4326需要转换到地图使用的 EPSG:3857
const features = new GeoJSON().readFeatures(geoJson, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857'
});
if (!features || features.length === 0) return;
// 2. 创建一个临时的矢量源来计算所有要素的包围盒
const source = new VectorSource({
features: features
});
// 3. 获取包围盒 [minX, minY, maxX, maxY]
const extent = source.getExtent();
// 检查包围盒是否有效
if (extent[0] === Infinity || extent[0] === -Infinity) {
console.warn('无法计算有效的地图包围盒');
return;
}
// 4. 调整视图
// padding: 设置四周留白,防止图形贴边
// duration: 动画持续时间
// maxZoom: 限制最大缩放级别,防止放大过度导致像素化
this.view.fit(extent, {
padding: [100, 200, 50, 50], // 上右下左留白
duration: 1000, // 1秒动画
maxZoom: 13 // 根据底图精度调整,避免过度放大
});
console.log('地图视野已调整至裁切区域');
} catch (e) {
console.error('调整地图视野失败:', e);
}
}
/**
* 清除地图遮罩(恢复底图正常显示)
*/
private clearMapMask(): void {
const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY) as TileLayer;
if (!baseLayer) return;
console.log('正在清除地图遮罩...');
if (this._maskPrerender) {
baseLayer.un('prerender', this._maskPrerender);
this._maskPrerender = null;
}
if (this._maskPostrender) {
baseLayer.un('postrender', this._maskPostrender);
this._maskPostrender = null;
}
// 强制重绘以移除遮罩效果
baseLayer.changed();
if (this.view) {
this.view.animate({
center: fromLonLat(CENTER_positionCN),
zoom: INITIAL_ZOOM,
duration: 1000 // 动画持续时间,保持与 fitViewToGeoJson 一致
});
console.log('地图视野已重置为初始状态');
}
}
/**
* 格式化长度输出 (米)
*/
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²';
}
mdLayerShowOrHidden(): void {}
switchView(type): void {
console.log(type);
}
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 {
console.log('销毁地图实例...');
// 1. 清除量算相关资源
this.removeQueryLayer();
this._maskPrerender = null;
this._maskPostrender = null;
// ✅ 关键:先清除所有图层的源,再移除图层,这能更有效地中断网络请求
const clearLayer = (layer: any) => {
if (layer && layer.getSource) {
const source = layer.getSource();
if (source && source.clear) {
source.clear(); // 清除缓存
}
// 对于 WMTS/XYZOL 内部有 TileQueuedispose source 有助于清理
if (source && source.dispose) {
// 注意:某些 OL 版本 source.dispose 可能不是公开 API但 clear 是必须的
}
}
if (this.map && this.map.getLayers().getArray().includes(layer)) {
this.map.removeLayer(layer);
}
// 强制解除引用
if (layer.setMap) layer.setMap(null);
};
// 2. 清除通过 layerRegistry 管理的图层
if (this.layerRegistry) {
this.layerRegistry.forEach(clearLayer);
this.layerRegistry.clear();
}
// 3. 清除点图层 Registry
if (this.pointLayerRegistry) {
this.pointLayerRegistry.forEach(clearLayer);
this.pointLayerRegistry.clear();
}
// 4. 清除地图实例
if (this.map) {
// 先清除交互和覆盖物
this.map.getInteractions().clear();
this.map.getOverlays().clear();
// 设置 target 为 undefined 会从 DOM 移除 canvas
this.map.setTarget(undefined);
// 销毁地图实例
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;
this.geoJsonData = null;
this.geoJsonData1 = null;
console.log('地图实例已销毁');
}
}