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

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