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

1473 lines
46 KiB
TypeScript
Raw Normal View History

2026-04-20 16:58:33 +08:00
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';
2026-04-22 17:53:20 +08:00
// import LayerGroup from 'ol/layer/Group';
// import OSM from 'ol/source/OSM';
2026-04-20 16:58:33 +08:00
import WMTS from 'ol/source/WMTS';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
2026-04-22 17:53:20 +08:00
import { get as getProjection, fromLonLat } from 'ol/proj';
2026-04-20 16:58:33 +08:00
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';
2026-04-20 16:58:33 +08:00
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”用于判断是否需要重新加载或只是切换显隐
// 基础图层配置
2026-04-20 16:58:33 +08:00
private baseLayerConfig: TileLayer | null = null;
// 基地配置
private hydropBaseConfig: any | null = null;
2026-04-20 16:58:33 +08:00
private REGISTRY_KEY = 'customBaseLayer';
// ✅ 新增:量算相关属性
2026-04-22 17:53:20 +08:00
private drawInteraction: any = null;
2026-04-20 16:58:33 +08:00
private measureLayer: VectorLayer | null = null;
private measureSource: VectorSource | null = null;
// ✅ 新增:存储点图层的 Registrykey 为 layerTypevalue 为 VectorLayer
private pointLayerRegistry: Map<string, VectorLayer> = new Map();
2026-04-22 17:53:20 +08:00
geoJsonData: any = null;
geoJsonData1: any = null;
// ✅ 新增:声明用于存储 mask 事件监听器的引用,以便后续移除
private _maskPrerender: any = null;
private _maskPostrender: any = null;
private BASEID: string = '';
2026-04-22 17:53:20 +08:00
constructor() {
// this.loadGeoJsonData();
2026-04-22 17:53:20 +08:00
this.loadGeoJsonData1();
}
private async loadGeoJsonData(): Promise<void> {
try {
const response = await fetch('/data/geoJson.json');
this.geoJsonData = await response.json();
2026-04-22 17:53:20 +08:00
} catch (error) {
console.error('配置加载失败:', error);
2026-04-22 17:53:20 +08:00
}
}
private async loadGeoJsonData2(url): Promise<any> {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('配置加载失败:', error);
}
}
2026-04-22 17:53:20 +08:00
private async loadGeoJsonData1(): Promise<void> {
try {
const response = await fetch('/data/geoJson1.json');
let res = await response.json();
let json1 = {
crs: res.crs,
features: [],
totalFeatures: res.totalFeatures,
type: res.type
};
for (let i = 0; i < res.features.length; i++) {
if (res.features[i].properties.BASEID === '12') {
json1.features.push(res.features[i]);
}
}
console.log(json1);
this.geoJsonData1 = json1;
2026-04-22 17:53:20 +08:00
} catch (error) {
console.error('配置加载失败:', error);
2026-04-22 17:53:20 +08:00
}
}
2026-04-20 16:58:33 +08:00
//地图初始化
2026-04-22 17:53:20 +08:00
init(container: HTMLElement): Promise<any> {
2026-04-20 16:58:33 +08:00
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, stnm, iconCode, titleName, ennm } = item;
2026-04-20 16:58:33 +08:00
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
2026-04-22 17:53:20 +08:00
// const currentResolution = this.view ? this.view.getResolution() : 1000;
2026-04-20 16:58:33 +08:00
// ✅ 2. 定义基准分辨率和基准缩放比例
// INITIAL_ZOOM (4.5) 对应的分辨率大约是 3000-4000 左右 (取决于投影)
// 这里我们用一个经验公式:分辨率越小(zoom越大)scale 越大
// 假设在初始分辨率下 scale 为 0.7
2026-04-22 17:53:20 +08:00
// const baseResolution = 3000; // 这是一个估算值,你可以根据实际效果微调
// const baseScale = 0.7;
2026-04-20 16:58:33 +08:00
// 计算动态缩放比例:
// 如果 currentResolution 变小 (放大地图)ratio 变大 -> icon 变大
// 限制最大和最小缩放,防止过大或过小
// let dynamicScale = baseScale * (baseResolution / (currentResolution || 1));
const currentZoom: any = this.view ? this.view.getZoom() : 4.5;
2026-04-20 16:58:33 +08:00
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}`;
}
// 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);
2026-04-20 16:58:33 +08:00
// 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;
2026-04-20 16:58:33 +08:00
}
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('未找到全量底图图层或图层类型错误');
}
}
2026-04-20 16:58:33 +08:00
/**
*
* @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.loadGeoJsonData2(url);
console.log('获取到的原始 GeoJSON:', geoJsonData);
// ✅ 新增:在应用遮罩前,先让地图视角聚焦到该区域
this.fitViewToGeoJson(geoJsonData);
console.log('基础图层', baseLayer);
// 应用遮罩
this.applyMapMask(baseLayer, geoJsonData);
} catch (error) {
console.error('加载裁切数据失败:', error);
// 出错时也清空遮罩,保证用户体验
this.clearMapMask();
}
2026-04-20 16:58:33 +08:00
}
/**
*
* @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 {
2026-04-22 17:53:20 +08:00
const layerInstance: any = this.pointLayerRegistry.get(layerType as string);
2026-04-20 16:58:33 +08:00
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({
2026-04-22 17:53:20 +08:00
features: new GeoJSON().readFeatures(this.geoJsonData, {
2026-04-20 16:58:33 +08:00
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({
2026-04-22 17:53:20 +08:00
source: this.measureSource!,
2026-04-20 16:58:33 +08:00
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;
2026-04-20 16:58:33 +08:00
// 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: [50, 50, 50, 50], // 上右下左留白
duration: 1000, // 1秒动画
maxZoom: 15 // 根据底图精度调整,避免过度放大
});
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();
}
2026-04-20 16:58:33 +08:00
/**
* ()
*/
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(): 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
}
);
}
2026-04-20 16:58:33 +08:00
/**
*
*/
destroy(): void {
console.log('销毁地图实例...');
// 1. 清除量算相关资源
this.removeQueryLayer();
this._maskPrerender = null;
this._maskPostrender = null;
2026-04-20 16:58:33 +08:00
// 2. 清除所有通过 layerRegistry 管理的图层
if (this.layerRegistry) {
this.layerRegistry.forEach(layer => {
2026-04-20 16:58:33 +08:00
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 => {
2026-04-20 16:58:33 +08:00
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();
2026-04-22 17:53:20 +08:00
this.map.setTarget(undefined);
2026-04-20 16:58:33 +08:00
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 方法待实现
}