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-05-25 08:54:17 +08:00
|
|
|
|
import Circle from 'ol/style/Circle';
|
|
|
|
|
|
import RegularShape from 'ol/style/RegularShape';
|
2026-06-01 08:42:06 +08:00
|
|
|
|
import Cluster from 'ol/source/Cluster';
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
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';
|
2026-05-11 17:45:09 +08:00
|
|
|
|
// 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'; // ✅ 确保引入获取图标路径的工具函数
|
2026-06-01 08:42:06 +08:00
|
|
|
|
import { generatePopupHtml } from '@/utils/popupHtmlGenerator';
|
|
|
|
|
|
import { useModelStore } from '@/store/modules/model';
|
|
|
|
|
|
|
|
|
|
|
|
const modelStore = useModelStore();
|
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;
|
|
|
|
|
|
// ✅ 新增:存储 key -> layer 实例,用于控制显示隐藏和切换
|
|
|
|
|
|
private layerRegistry: Map<string, any> = new Map();
|
|
|
|
|
|
// ✅ 新增:记录当前正在显示的“切换 Key”,用于判断是否需要重新加载或只是切换显隐
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 基础图层配置
|
2026-04-20 16:58:33 +08:00
|
|
|
|
private baseLayerConfig: TileLayer | null = null;
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 基地配置
|
|
|
|
|
|
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;
|
|
|
|
|
|
// ✅ 新增:存储点图层的 Registry,key 为 layerType,value 为 VectorLayer
|
|
|
|
|
|
private pointLayerRegistry: Map<string, VectorLayer> = new Map();
|
2026-04-22 17:53:20 +08:00
|
|
|
|
geoJsonData: any = null;
|
|
|
|
|
|
geoJsonData1: any = null;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
// ✅ 新增:声明用于存储 mask 事件监听器的引用,以便后续移除
|
|
|
|
|
|
private _maskPrerender: any = null;
|
|
|
|
|
|
private _maskPostrender: any = null;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
private BASEID = '';
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// ✅ 新增:记录当前鼠标悬停的 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> {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
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 {
|
|
|
|
|
|
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 默认也有动画
|
2026-05-25 08:54:17 +08:00
|
|
|
|
maxDelta: 2,
|
2026-04-20 16:58:33 +08:00
|
|
|
|
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());
|
|
|
|
|
|
});
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 点击弹出弹框
|
|
|
|
|
|
this.map.on('click', evt => {
|
|
|
|
|
|
const pixel = evt.pixel;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用共享方法检测要素
|
|
|
|
|
|
const { detectedFeature, isHitIcon } = this.detectFeatureAtPixel(pixel);
|
|
|
|
|
|
|
|
|
|
|
|
if (detectedFeature && isHitIcon) {
|
|
|
|
|
|
modelStore.modalVisible = true;
|
|
|
|
|
|
modelStore.params = detectedFeature.values_;
|
|
|
|
|
|
modelStore.title = detectedFeature.values_.titleName + ' 详情信息';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 新增:监听鼠标移动,动态改变光标样式
|
2026-05-15 17:38:03 +08:00
|
|
|
|
let lastHoveredId: string | number | null = null;
|
|
|
|
|
|
let animationFrameId: number | null = null;
|
|
|
|
|
|
// pointermove
|
2026-06-01 08:42:06 +08:00
|
|
|
|
this.map.on('pointermove', evt => {
|
2026-04-20 16:58:33 +08:00
|
|
|
|
const pixel = evt.pixel;
|
|
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 判断地图层级,4.7 以上才进行 popup 显示
|
|
|
|
|
|
const zoom = this.map.getView().getZoom();
|
|
|
|
|
|
if (zoom !== undefined && zoom < 4.7) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// 取消之前的动画帧,确保只处理最新的一次移动
|
|
|
|
|
|
if (animationFrameId) {
|
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
animationFrameId = requestAnimationFrame(() => {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 使用共享方法检测要素
|
|
|
|
|
|
const { detectedFeature, isHitIcon } =
|
|
|
|
|
|
this.detectFeatureAtPixel(pixel);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
});
|
2026-04-20 16:58:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
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 {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
console.log('addInitDataLayer', pointData, layerType, mdoptions);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// const shouldClear = mdoptions?.isRemove !== false;
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
if (!vectorLayer) {
|
|
|
|
|
|
vectorSource = new VectorSource();
|
|
|
|
|
|
vectorLayer = new VectorLayer({
|
|
|
|
|
|
source: vectorSource,
|
|
|
|
|
|
zIndex: 100, // 确保点在底图之上
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// declutter: false,
|
|
|
|
|
|
// renderOrder: (a, b) =>
|
|
|
|
|
|
// (b.get('distance') || 0) - (a.get('distance') || 0),
|
|
|
|
|
|
style: (feature: any) => this.createPointStyle(feature) // 使用动态样式函数
|
|
|
|
|
|
// style: (feature: any) => this.createPointStyle1(feature) // 使用动态样式函数
|
2026-04-20 16:58:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
this.pointLayerRegistry.set(targetLayerKey, vectorLayer);
|
|
|
|
|
|
this.map.addLayer(vectorLayer);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vectorSource = vectorLayer.getSource() as VectorSource;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// if (shouldClear) {
|
|
|
|
|
|
// vectorSource.clear();
|
|
|
|
|
|
// }
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dataArray.length === 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`开始加载 OL 图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 遍历数据生成 Feature
|
|
|
|
|
|
const features: Feature[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
dataArray.forEach((item: any) => {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
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
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const labelText = titleName || stnm || ennm || '';
|
|
|
|
|
|
if (lgtd == null || lttd == null) return;
|
|
|
|
|
|
const lon = Number(lgtd);
|
|
|
|
|
|
const lat = Number(lttd);
|
|
|
|
|
|
|
|
|
|
|
|
if (isNaN(lon) || isNaN(lat) || !isFinite(lon) || !isFinite(lat)) return;
|
|
|
|
|
|
if (Math.abs(lon) > 180 || Math.abs(lat) > 90) return;
|
|
|
|
|
|
const coord = fromLonLat([lon, lat]);
|
|
|
|
|
|
if (!isFinite(coord[0]) || !isFinite(coord[1])) return;
|
|
|
|
|
|
const geometry = new Point(coord);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
// 5. 创建 Feature
|
|
|
|
|
|
const feature = new Feature({
|
2026-06-01 08:42:06 +08:00
|
|
|
|
geometry: geometry, // ✅ 关键:坐标转换 EPSG:4326 -> EPSG:3857
|
2026-04-20 16:58:33 +08:00
|
|
|
|
...item, // 保留原始数据以便点击事件使用
|
|
|
|
|
|
_layerKey: targetLayerKey
|
|
|
|
|
|
});
|
2026-05-15 17:38:03 +08:00
|
|
|
|
const uniqueId = item.stcd || item._id;
|
|
|
|
|
|
|
|
|
|
|
|
feature.setId(uniqueId);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 将自定义属性绑定到 feature,以便样式函数访问
|
|
|
|
|
|
feature.set('_iconUrl', iconUrl);
|
|
|
|
|
|
feature.set('_labelText', labelText);
|
2026-06-01 08:42:06 +08:00
|
|
|
|
feature.set('_legendVisible', true); // 默认显示
|
|
|
|
|
|
feature.set('_sttpMap', item.sttpMap); // 默认显示
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (item.popupHtml) {
|
|
|
|
|
|
feature.set('popupHtml', item.popupHtml);
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
features.push(feature);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 批量添加要素
|
|
|
|
|
|
if (features.length > 0) {
|
|
|
|
|
|
vectorSource.addFeatures(features);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
2026-06-01 08:42:06 +08:00
|
|
|
|
console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化 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
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
});
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (this.map) {
|
|
|
|
|
|
this.map.addOverlay(this.popupOverlay);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 在指定像素位置检测要素(共享方法)
|
|
|
|
|
|
* @param pixel 像素坐标
|
|
|
|
|
|
* @returns 检测到的要素和是否命中图标区域
|
|
|
|
|
|
*/
|
|
|
|
|
|
private detectFeatureAtPixel(pixel: number[]): {
|
|
|
|
|
|
detectedFeature: Feature | undefined;
|
|
|
|
|
|
isHitIcon: boolean;
|
|
|
|
|
|
} {
|
|
|
|
|
|
let detectedFeature: Feature | undefined = undefined;
|
|
|
|
|
|
let isHitIcon = 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();
|
|
|
|
|
|
const 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,完全依赖上面的距离计算
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return { detectedFeature, isHitIcon };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 在 pointermove 中调用此方法显示/隐藏 Popup
|
|
|
|
|
|
* @param feature 当前悬停的要素
|
|
|
|
|
|
* @param coordinate 地理坐标
|
|
|
|
|
|
*/
|
|
|
|
|
|
showPopup(feature: Feature | undefined, coordinate: number[] | undefined) {
|
|
|
|
|
|
if (!this.popupOverlay || !this.popupElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (feature && coordinate) {
|
|
|
|
|
|
const props = feature.getProperties();
|
|
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const popupHtml = props.popupHtml || generatePopupHtml(props);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
if (popupHtml) {
|
|
|
|
|
|
this.popupElement.innerHTML = popupHtml;
|
|
|
|
|
|
this.popupOverlay.setPosition(coordinate);
|
|
|
|
|
|
this.popupElement.style.display = 'block';
|
|
|
|
|
|
} else {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 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';
|
2026-05-15 17:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.popupOverlay.setPosition(undefined);
|
|
|
|
|
|
this.popupElement.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private createPointStyle(feature: Feature): Style {
|
|
|
|
|
|
const iconUrl = feature.get('_iconUrl') as string;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const sttpMap = feature.get('_sttpMap') as string;
|
2026-04-20 16:58:33 +08:00
|
|
|
|
const labelText = feature.get('_labelText') as string;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const legendVisible = feature.get('_legendVisible');
|
|
|
|
|
|
const regionVisible = feature.get('_regionVisible');
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果没图标,返回空样式
|
|
|
|
|
|
if (!iconUrl) {
|
|
|
|
|
|
return new Style();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 如果图例设置为隐藏,返回空样式
|
|
|
|
|
|
if (legendVisible === false) {
|
|
|
|
|
|
return new Style();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果区域外,返回空样式
|
|
|
|
|
|
if (regionVisible === false) {
|
|
|
|
|
|
return new Style();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 16:58:33 +08:00
|
|
|
|
// ✅ 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));
|
2026-05-11 17:45:09 +08:00
|
|
|
|
const currentZoom: any = this.view ? this.view.getZoom() : 4.5;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const distance = feature.get('distance'); // 你通过计算赋值的字段
|
|
|
|
|
|
// ✅ 分级过滤规则(根据实际业务调整阈值)
|
|
|
|
|
|
if (distance !== undefined && distance !== null) {
|
|
|
|
|
|
let minDistanceRequired = 0;
|
|
|
|
|
|
if (currentZoom <= 6) {
|
|
|
|
|
|
// 极远视图:只显示最孤立的站点(distance >= 50000000 或 1500000?根据你的数据分布定)
|
|
|
|
|
|
// 假设 50000000 和 1500000 属于高值,都显示;400000 和 50000 不显示
|
|
|
|
|
|
minDistanceRequired = 1500000; // 只显示 >=150万 的点
|
|
|
|
|
|
} else if (currentZoom <= 8) {
|
|
|
|
|
|
minDistanceRequired = 800000; // 显示 >=80万 的点
|
|
|
|
|
|
} else if (currentZoom <= 10.5) {
|
|
|
|
|
|
minDistanceRequired = 400000; // 显示 >=40万 的点
|
|
|
|
|
|
} else {
|
|
|
|
|
|
minDistanceRequired = 0; // 放大到14级以上显示所有点(包括5万的)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (distance <= minDistanceRequired) {
|
|
|
|
|
|
return new Style(); // 不满足,隐藏
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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));
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
const currentFeatureId = feature.getId() || (feature as any).ol_uid;
|
|
|
|
|
|
const isHovered = this.hoveredFeatureId === currentFeatureId;
|
|
|
|
|
|
const zIndex = isHovered ? 100 : 0;
|
|
|
|
|
|
const styleOptions: any = {
|
2026-04-20 16:58:33 +08:00
|
|
|
|
image: new Icon({
|
|
|
|
|
|
src: iconUrl,
|
2026-05-15 17:38:03 +08:00
|
|
|
|
scale: dynamicScale,
|
|
|
|
|
|
anchor: [0.5, 0.5],
|
2026-06-01 08:42:06 +08:00
|
|
|
|
crossOrigin: 'anonymous',
|
|
|
|
|
|
declutterMode: 'obstacle' // 图标参与碰撞,可被隐藏
|
2026-04-20 16:58:33 +08:00
|
|
|
|
})
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 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' // 避免标签重叠
|
2026-04-20 16:58:33 +08:00
|
|
|
|
});
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// }
|
|
|
|
|
|
return new Style(styleOptions);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-05-25 08:54:17 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化加载基础图层
|
|
|
|
|
|
* @param layer 图层配置对象
|
|
|
|
|
|
*/
|
2026-06-01 08:42:06 +08:00
|
|
|
|
addBaseDataLayer(
|
|
|
|
|
|
layer: any,
|
|
|
|
|
|
isShow: boolean,
|
|
|
|
|
|
isCache: boolean = false
|
|
|
|
|
|
): void {
|
2026-04-20 16:58:33 +08:00
|
|
|
|
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 {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
wmtsLayer.setZIndex(-100);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
wmtsLayer.type = 'layer';
|
2026-04-20 16:58:33 +08:00
|
|
|
|
// 5. 注册与添加
|
2026-06-01 08:42:06 +08:00
|
|
|
|
wmtsLayer.setVisible(isShow);
|
|
|
|
|
|
console.log('基础图层', wmtsLayer);
|
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);
|
2026-06-01 08:42:06 +08:00
|
|
|
|
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') {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
this.hydropBaseConfig = layer;
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
console.log(`矢量图层 [${layer.key}] 已加载: ${layer.url}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
// 在 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 的图层,隐藏其他)
|
|
|
|
|
|
*/
|
2026-05-13 08:44:35 +08:00
|
|
|
|
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) {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// ✅ 情况 A: isAll == false -> 清空裁切,显示所有锚点
|
2026-05-13 08:44:35 +08:00
|
|
|
|
this.clearMapMask();
|
2026-06-01 08:42:06 +08:00
|
|
|
|
this.showAllPoints();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 获取底图图层 (用于应用遮罩的目标)
|
|
|
|
|
|
const baseLayer = this.layerRegistry.get(this.REGISTRY_KEY) as TileLayer;
|
|
|
|
|
|
if (!baseLayer) {
|
|
|
|
|
|
console.warn('未找到底图图层,无法应用遮罩');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 关键优化:在加载新区域数据前,先隐藏所有锚点,避免闪烁
|
|
|
|
|
|
this.hideAllPoints();
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const url =
|
|
|
|
|
|
this.hydropBaseConfig.geojson_url +
|
|
|
|
|
|
`&cql_filter=BASEID='${this.BASEID}'`;
|
|
|
|
|
|
console.log('正在请求裁切数据:', url);
|
|
|
|
|
|
|
|
|
|
|
|
// 等待数据加载
|
2026-05-15 17:38:03 +08:00
|
|
|
|
const geoJsonData = await this.loadGeoJsonData(url);
|
2026-05-13 08:44:35 +08:00
|
|
|
|
console.log('获取到的原始 GeoJSON:', geoJsonData);
|
|
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// ✅ 先根据区域边界过滤锚点(此时锚点还是隐藏的)
|
|
|
|
|
|
this.filterPointsByRegion(geoJsonData);
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 再让地图视角聚焦到该区域
|
2026-05-13 08:44:35 +08:00
|
|
|
|
this.fitViewToGeoJson(geoJsonData);
|
|
|
|
|
|
|
|
|
|
|
|
console.log('基础图层', baseLayer);
|
|
|
|
|
|
// 应用遮罩
|
|
|
|
|
|
this.applyMapMask(baseLayer, geoJsonData);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载裁切数据失败:', error);
|
|
|
|
|
|
// 出错时也清空遮罩,保证用户体验
|
|
|
|
|
|
this.clearMapMask();
|
2026-06-01 08:42:06 +08:00
|
|
|
|
this.showAllPoints();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 隐藏所有锚点
|
|
|
|
|
|
*/
|
|
|
|
|
|
private hideAllPoints(): void {
|
|
|
|
|
|
this.pointLayerRegistry.forEach(layer => {
|
|
|
|
|
|
const vectorSource = layer.getSource() as VectorSource;
|
|
|
|
|
|
if (!vectorSource) return;
|
|
|
|
|
|
|
|
|
|
|
|
const features = vectorSource.getFeatures();
|
|
|
|
|
|
features.forEach((feature: Feature) => {
|
|
|
|
|
|
feature.set('_regionVisible', false);
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示所有锚点
|
|
|
|
|
|
*/
|
|
|
|
|
|
private showAllPoints(): void {
|
|
|
|
|
|
this.pointLayerRegistry.forEach(layer => {
|
|
|
|
|
|
const vectorSource = layer.getSource() as VectorSource;
|
|
|
|
|
|
if (!vectorSource) return;
|
|
|
|
|
|
|
|
|
|
|
|
const features = vectorSource.getFeatures();
|
|
|
|
|
|
features.forEach((feature: Feature) => {
|
|
|
|
|
|
feature.set('_regionVisible', true);
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据区域边界过滤锚点显示
|
|
|
|
|
|
* 使用射线法判断点是否在多边形内
|
|
|
|
|
|
*/
|
|
|
|
|
|
private filterPointsByRegion(geoJson: any): void {
|
|
|
|
|
|
// 解析 GeoJSON 获取区域多边形坐标
|
|
|
|
|
|
const regionCoords = this.extractPolygonCoords(geoJson);
|
|
|
|
|
|
if (!regionCoords || regionCoords.length === 0) {
|
|
|
|
|
|
console.warn('无法解析区域边界,显示所有锚点');
|
|
|
|
|
|
this.showAllPoints();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历所有锚点图层
|
|
|
|
|
|
this.pointLayerRegistry.forEach(layer => {
|
|
|
|
|
|
const vectorSource = layer.getSource() as VectorSource;
|
|
|
|
|
|
if (!vectorSource) return;
|
|
|
|
|
|
|
|
|
|
|
|
const features = vectorSource.getFeatures();
|
|
|
|
|
|
features.forEach((feature: Feature) => {
|
|
|
|
|
|
const props = feature.getProperties();
|
|
|
|
|
|
const lon = props.lgtd;
|
|
|
|
|
|
const lat = props.lttd;
|
|
|
|
|
|
|
|
|
|
|
|
if (lon == null || lat == null) {
|
|
|
|
|
|
feature.set('_regionVisible', false);
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断点是否在区域内
|
|
|
|
|
|
const isInside = this.isPointInPolygon(lon, lat, regionCoords);
|
|
|
|
|
|
feature.set('_regionVisible', isInside);
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 GeoJSON 中提取多边形坐标
|
|
|
|
|
|
*/
|
|
|
|
|
|
private extractPolygonCoords(geoJson: any): number[][][] {
|
|
|
|
|
|
if (!geoJson || !geoJson.features || geoJson.features.length === 0) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const coords: number[][][] = [];
|
|
|
|
|
|
geoJson.features.forEach((feature: any) => {
|
|
|
|
|
|
const geometry = feature.geometry;
|
|
|
|
|
|
if (!geometry) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (geometry.type === 'Polygon') {
|
|
|
|
|
|
// Polygon: [[[lon, lat], ...], ...]
|
|
|
|
|
|
coords.push(...geometry.coordinates);
|
|
|
|
|
|
} else if (geometry.type === 'MultiPolygon') {
|
|
|
|
|
|
// MultiPolygon: [[[[lon, lat], ...], ...], ...]
|
|
|
|
|
|
geometry.coordinates.forEach((polygon: number[][][]) => {
|
|
|
|
|
|
coords.push(...polygon);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return coords;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 射线法判断点是否在多边形内
|
|
|
|
|
|
* @param lon 经度
|
|
|
|
|
|
* @param lat 纬度
|
|
|
|
|
|
* @param polygons 多边形坐标数组 [[[lon, lat], ...], ...]
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isPointInPolygon(
|
|
|
|
|
|
lon: number,
|
|
|
|
|
|
lat: number,
|
|
|
|
|
|
polygons: number[][][]
|
|
|
|
|
|
): boolean {
|
|
|
|
|
|
for (const polygon of polygons) {
|
|
|
|
|
|
if (this.isPointInRing(lon, lat, polygon)) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 射线法判断点是否在单个环内
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isPointInRing(lon: number, lat: number, ring: number[][]): boolean {
|
|
|
|
|
|
let inside = false;
|
|
|
|
|
|
const n = ring.length;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
|
|
|
|
const xi = ring[i][0];
|
|
|
|
|
|
const yi = ring[i][1];
|
|
|
|
|
|
const xj = ring[j][0];
|
|
|
|
|
|
const yj = ring[j][1];
|
|
|
|
|
|
|
|
|
|
|
|
const intersect =
|
|
|
|
|
|
yi > lat !== yj > lat &&
|
|
|
|
|
|
lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
|
|
|
|
|
|
|
|
|
|
|
if (intersect) {
|
|
|
|
|
|
inside = !inside;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return inside;
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 基础图层显示影隐藏方法
|
|
|
|
|
|
* @param layerType 图层类型 (备用,优先使用 key)
|
|
|
|
|
|
* @param key 图层唯一标识 Key
|
|
|
|
|
|
* @param checked true 为显示,false 为隐藏
|
|
|
|
|
|
*/
|
|
|
|
|
|
controlBaseLayerTreeShowAndHidden(
|
2026-06-01 08:42:06 +08:00
|
|
|
|
layerType: string,
|
|
|
|
|
|
key: string,
|
2026-04-20 16:58:33 +08:00
|
|
|
|
checked: boolean
|
|
|
|
|
|
): void {
|
|
|
|
|
|
if (!this.map) return;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
console.log('controlBaseLayerTreeShowAndHidden', layerType, key, checked);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-06-01 08:42:06 +08:00
|
|
|
|
console.log(layerInstance);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
if (layerInstance) {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
layerInstance.setVisible(checked);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
// 3. 设置可见性
|
|
|
|
|
|
// OpenLayers 的 Layer.setVisible(boolean) 方法
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// layerInstance.setVisible(checked);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`图层 [${registryKey}] 已设置为: ${checked ? '显示' : '隐藏'}`
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`未找到标识为 [${registryKey}] 的图层实例,当前注册表 keys:`,
|
|
|
|
|
|
Array.from(this.layerRegistry.keys())
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 图层树控制描点数据显示隐藏方法
|
2026-06-01 08:42:06 +08:00
|
|
|
|
mdLayerTreeShowOrHidden(layerType: string, checked?: boolean): void {
|
|
|
|
|
|
console.log('mdLayerTreeShowOrHidden', layerType, checked);
|
|
|
|
|
|
const layerInstance: any = this.pointLayerRegistry.get(layerType);
|
|
|
|
|
|
console.log('mdLayerTreeShowOrHidden', layerInstance);
|
|
|
|
|
|
if (layerInstance) {
|
|
|
|
|
|
layerInstance.setVisible(checked);
|
|
|
|
|
|
}
|
2026-04-20 16:58:33 +08:00
|
|
|
|
// this.service.mdLayerTreeShowOrHidden(layerType, checked)
|
|
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
|
|
|
|
|
|
hasLayer(layerType: string): void {
|
|
|
|
|
|
console.log('hasLayer', layerType);
|
|
|
|
|
|
const layerInstance: any = this.pointLayerRegistry.get(layerType);
|
|
|
|
|
|
console.log('hasLayer', layerInstance);
|
|
|
|
|
|
return this.pointLayerRegistry.has(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')
|
|
|
|
|
|
*/
|
2026-06-01 08:42:06 +08:00
|
|
|
|
baseLayerSwitcher(key: string, checked: boolean): void {
|
2026-04-20 16:58:33 +08:00
|
|
|
|
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') {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
this.addBaseDataLayer(this.baseLayerConfig, checked);
|
|
|
|
|
|
// 隐藏 BASEMAP-white 和 BASEMAP-img 图层(如果存在)
|
|
|
|
|
|
this.hideBaseMapLayers(['BASEMAP-white', 'BASEMAP-img']);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
} else {
|
2026-06-01 08:42:06 +08:00
|
|
|
|
this.addBaseDataLayer(this.baseLayerConfig, checked, true);
|
|
|
|
|
|
this.addBaseDataLayer(servers.BaseLayer[key], 1);
|
|
|
|
|
|
// 显示 BASEMAP-white 和 BASEMAP-img 图层(如果存在)
|
|
|
|
|
|
this.showBaseMapLayers(['BASEMAP-white', 'BASEMAP-img']);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-01 08:42:06 +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. 检查是否已存在该图层,避免重复添加
|
|
|
|
|
|
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 = '×';
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 为栅格图层添加基于矢量的裁切效果
|
|
|
|
|
|
* @param rasterLayer 需要被裁切的底图图层 (TileLayer)
|
|
|
|
|
|
* @param clipGeoJson GeoJSON 格式的裁切边界数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
private applyMapMask(rasterLayer: TileLayer, clipGeoJson: any): void {
|
|
|
|
|
|
if (!rasterLayer || !clipGeoJson) return;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
console.log('地图遮罩开始');
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 1. 解析 GeoJSON 为 OL Features (EPSG:3857)
|
2026-05-11 17:45:09 +08:00
|
|
|
|
const features = new GeoJSON().readFeatures(clipGeoJson, {
|
|
|
|
|
|
dataProjection: 'EPSG:4326',
|
|
|
|
|
|
featureProjection: 'EPSG:3857'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
if (!features || features.length === 0) {
|
|
|
|
|
|
console.warn('裁切数据为空,无法应用遮罩');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 移除旧的监听器,防止重复绑定
|
2026-05-11 17:45:09 +08:00
|
|
|
|
if (this._maskPrerender) {
|
|
|
|
|
|
rasterLayer.un('prerender', this._maskPrerender);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this._maskPostrender) {
|
|
|
|
|
|
rasterLayer.un('postrender', this._maskPostrender);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 3. 定义 prerender 事件处理函数
|
|
|
|
|
|
// OpenLayers 在 prerender 时,context 已经包含了当前帧的状态
|
2026-05-11 17:45:09 +08:00
|
|
|
|
const maskPrerender = (event: any) => {
|
|
|
|
|
|
const context = event.context;
|
|
|
|
|
|
const frameState = event.frameState;
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
if (!context || !frameState || !this.map) return;
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 关键步骤 1: 保存当前 Canvas 状态
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context.save();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 关键步骤 2: 重置变换矩阵,确保我们操作的是屏幕像素坐标
|
|
|
|
|
|
// OpenLayers 默认可能应用了平移/缩放,我们需要手动控制
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
let hasValidPath = false;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 遍历所有特征,绘制路径
|
2026-05-11 17:45:09 +08:00
|
|
|
|
for (const feature of features) {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
const geom = feature.getGeometry();
|
2026-05-11 17:45:09 +08:00
|
|
|
|
if (!geom) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const type = geom.getType();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理 Polygon
|
2026-05-11 17:45:09 +08:00
|
|
|
|
if (type === 'Polygon') {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
const rings = (geom as any).getCoordinates() as number[][][];
|
2026-05-11 17:45:09 +08:00
|
|
|
|
for (const ring of rings) {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
if (this.drawRingToContext(context, ring, frameState)) {
|
|
|
|
|
|
hasValidPath = true;
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
}
|
2026-05-13 08:44:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 处理 MultiPolygon
|
|
|
|
|
|
else if (type === 'MultiPolygon') {
|
|
|
|
|
|
const polygons = (geom as any).getCoordinates() as number[][][][];
|
2026-05-11 17:45:09 +08:00
|
|
|
|
for (const polygonRings of polygons) {
|
|
|
|
|
|
for (const ring of polygonRings) {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
if (this.drawRingToContext(context, ring, frameState)) {
|
|
|
|
|
|
hasValidPath = true;
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 关键步骤 3: 如果绘制了有效路径,则执行裁剪
|
|
|
|
|
|
if (hasValidPath) {
|
|
|
|
|
|
// 闭合路径(虽然 drawRing 里做了 closePath,但为了保险)
|
|
|
|
|
|
|
|
|
|
|
|
context.save();
|
|
|
|
|
|
// 先用外描边创建裁剪区域
|
|
|
|
|
|
context.lineWidth = 6;
|
|
|
|
|
|
context.strokeStyle = '#6D64DF';
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context.stroke();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
context.clip(); // 裁剪到外描边的内部区域
|
|
|
|
|
|
context.strokeStyle = '#CCC9F4';
|
|
|
|
|
|
context.lineWidth = 5;
|
|
|
|
|
|
context.stroke();
|
|
|
|
|
|
// 执行裁剪!此后所有绘制操作仅在此路径内可见
|
|
|
|
|
|
context.clip();
|
|
|
|
|
|
|
|
|
|
|
|
// 可选:如果你希望裁剪区域外有半透明黑色遮罩,可以在这里绘制一个全屏矩形
|
|
|
|
|
|
// 但通常我们只希望“隐藏”外部,所以只需 clip 即可,OL 会自动绘制瓦片
|
2026-05-11 17:45:09 +08:00
|
|
|
|
} else {
|
2026-05-13 08:44:35 +08:00
|
|
|
|
console.warn('未能生成有效的裁切路径');
|
2026-05-11 17:45:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 4. 定义 postrender 事件处理函数
|
|
|
|
|
|
// 用于恢复 Canvas 状态,避免影响其他图层或后续帧
|
2026-05-11 17:45:09 +08:00
|
|
|
|
const maskPostrender = (event: any) => {
|
|
|
|
|
|
if (event.context) {
|
|
|
|
|
|
event.context.restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 5. 绑定新的事件监听器
|
2026-05-11 17:45:09 +08:00
|
|
|
|
this._maskPrerender = maskPrerender;
|
|
|
|
|
|
this._maskPostrender = maskPostrender;
|
|
|
|
|
|
|
|
|
|
|
|
rasterLayer.on('prerender', this._maskPrerender);
|
|
|
|
|
|
rasterLayer.on('postrender', this._maskPostrender);
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 6. 强制触发重绘
|
2026-05-11 17:45:09 +08:00
|
|
|
|
rasterLayer.changed();
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('地图遮罩已应用');
|
2026-05-11 17:45:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 将单个环(Ring)绘制到 Canvas 上下文
|
|
|
|
|
|
* @returns boolean 是否成功绘制了有效路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
private drawRingToContext(
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context: CanvasRenderingContext2D,
|
|
|
|
|
|
ring: number[][],
|
|
|
|
|
|
frameState: any
|
2026-05-13 08:44:35 +08:00
|
|
|
|
): boolean {
|
|
|
|
|
|
if (!ring || ring.length < 3) return false; // 至少需要3个点构成面
|
2026-05-11 17:45:09 +08:00
|
|
|
|
|
|
|
|
|
|
const pixelRatio = frameState.pixelRatio || window.devicePixelRatio || 1;
|
2026-05-13 08:44:35 +08:00
|
|
|
|
let moved = false;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < ring.length; i++) {
|
|
|
|
|
|
const coord = ring[i];
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 坐标有效性检查
|
|
|
|
|
|
if (!coord || coord.length < 2 || typeof coord[0] !== 'number') continue;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 获取 CSS 像素坐标
|
|
|
|
|
|
const cssPixel = this.map?.getPixelFromCoordinate(
|
2026-05-11 17:45:09 +08:00
|
|
|
|
coord as [number, number]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!cssPixel) continue;
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
// 转换为物理像素坐标
|
2026-05-11 17:45:09 +08:00
|
|
|
|
const canvasX = cssPixel[0] * pixelRatio;
|
|
|
|
|
|
const canvasY = cssPixel[1] * pixelRatio;
|
|
|
|
|
|
|
2026-05-13 08:44:35 +08:00
|
|
|
|
if (!moved) {
|
2026-05-11 17:45:09 +08:00
|
|
|
|
context.moveTo(canvasX, canvasY);
|
2026-05-13 08:44:35 +08:00
|
|
|
|
moved = true;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
context.lineTo(canvasX, canvasY);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-13 08:44:35 +08:00
|
|
|
|
|
|
|
|
|
|
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, {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
padding: [100, 200, 50, 50], // 上右下左留白
|
2026-05-13 08:44:35 +08:00
|
|
|
|
duration: 1000, // 1秒动画
|
2026-05-15 17:38:03 +08:00
|
|
|
|
maxZoom: 13 // 根据底图精度调整,避免过度放大
|
2026-05-13 08:44:35 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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-05-15 17:38:03 +08:00
|
|
|
|
if (this.view) {
|
|
|
|
|
|
this.view.animate({
|
|
|
|
|
|
center: fromLonLat(CENTER_positionCN),
|
|
|
|
|
|
zoom: INITIAL_ZOOM,
|
|
|
|
|
|
duration: 1000 // 动画持续时间,保持与 fitViewToGeoJson 一致
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('地图视野已重置为初始状态');
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
}
|
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²';
|
|
|
|
|
|
}
|
2026-06-01 08:42:06 +08:00
|
|
|
|
// 图例和基地面板控制描点数据显示隐藏方法
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 图例和基地面板控制描点数据显示隐藏方法
|
|
|
|
|
|
* @param layerType 图层类型
|
|
|
|
|
|
* @param key 图层标识符
|
|
|
|
|
|
* @param baseid 基地ID
|
|
|
|
|
|
* @param checked 是否选中(显示/隐藏)
|
|
|
|
|
|
* @param isAll 是否全部操作
|
|
|
|
|
|
*/
|
|
|
|
|
|
mdLayerShowOrHidden(
|
|
|
|
|
|
layerType: string,
|
|
|
|
|
|
baseid: string,
|
|
|
|
|
|
key?: string,
|
|
|
|
|
|
checked?: boolean,
|
|
|
|
|
|
isAll?: boolean
|
|
|
|
|
|
): void {
|
|
|
|
|
|
console.log('mdLayerShowOrHidden', layerType, key, baseid, checked, isAll);
|
|
|
|
|
|
// 确定查找的 Key:优先使用传入的 key,如果为空则尝试使用 layerType
|
|
|
|
|
|
const registryKey = key || layerType;
|
|
|
|
|
|
|
|
|
|
|
|
if (isAll) {
|
|
|
|
|
|
// 如果是全部操作,则对所有相应类型的图层进行处理
|
|
|
|
|
|
if (layerType === 'point') {
|
|
|
|
|
|
// 处理所有点图层
|
|
|
|
|
|
this.pointLayerRegistry.forEach((layer, key) => {
|
|
|
|
|
|
if (layer) {
|
|
|
|
|
|
layer.setVisible(checked);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 处理其他类型的图层
|
|
|
|
|
|
this.layerRegistry.forEach((layer, key) => {
|
|
|
|
|
|
if (key.startsWith(layerType as string)) {
|
|
|
|
|
|
layer.setVisible(checked);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 单独操作特定图层
|
|
|
|
|
|
let targetLayer = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 先尝试在点图层注册表中查找
|
|
|
|
|
|
targetLayer = this.pointLayerRegistry.get(registryKey as string);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没找到,再在普通图层注册表中查找
|
|
|
|
|
|
if (!targetLayer) {
|
|
|
|
|
|
targetLayer = this.layerRegistry.get(registryKey as string);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果指定了基地ID,可以进一步筛选
|
|
|
|
|
|
if (targetLayer) {
|
|
|
|
|
|
targetLayer.setVisible(checked);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据 anchoPointState 控制单个图例的锚点显示隐藏
|
|
|
|
|
|
* @param layerKey 图层 key
|
|
|
|
|
|
* @param anchoPointState 锚点状态(对应图例的 nameEn)
|
|
|
|
|
|
* @param checked 是否显示
|
|
|
|
|
|
*/
|
|
|
|
|
|
setLegendPointVisible(
|
|
|
|
|
|
layerKey: string,
|
|
|
|
|
|
anchoPointState: string,
|
|
|
|
|
|
checked: boolean
|
|
|
|
|
|
): void {
|
|
|
|
|
|
const vectorLayer = this.pointLayerRegistry.get(layerKey);
|
|
|
|
|
|
if (!vectorLayer) return;
|
2026-05-11 17:45:09 +08:00
|
|
|
|
|
2026-06-01 08:42:06 +08:00
|
|
|
|
const vectorSource = vectorLayer.getSource() as VectorSource;
|
|
|
|
|
|
if (!vectorSource) return;
|
|
|
|
|
|
|
|
|
|
|
|
const features = vectorSource.getFeatures();
|
|
|
|
|
|
features.forEach((feature: Feature) => {
|
|
|
|
|
|
const props = feature.getProperties();
|
|
|
|
|
|
// 如果 anchoPointState 为空,隐藏所有锚点
|
|
|
|
|
|
if (!anchoPointState) {
|
|
|
|
|
|
feature.set('_legendVisible', false);
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
} else if (props.anchoPointState === anchoPointState) {
|
|
|
|
|
|
// 根据 anchoPointState 匹配
|
|
|
|
|
|
// 设置 feature 的可见性
|
|
|
|
|
|
feature.set('_legendVisible', checked);
|
|
|
|
|
|
// 触发重新渲染
|
|
|
|
|
|
feature.changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
// 不匹配的 feature 不做处理,保持原样
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
switchView(type): void {
|
|
|
|
|
|
console.log(type);
|
|
|
|
|
|
}
|
2026-05-11 17:45:09 +08:00
|
|
|
|
fitBounds(): void {}
|
2026-05-13 08:44:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 飞行定位到指定经纬度和缩放级别
|
|
|
|
|
|
* @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-05-25 08:54:17 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成带完整圆边框的上半圆 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 {
|
|
|
|
|
|
console.log('销毁地图实例...');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 清除量算相关资源
|
|
|
|
|
|
this.removeQueryLayer();
|
|
|
|
|
|
|
2026-05-11 17:45:09 +08:00
|
|
|
|
this._maskPrerender = null;
|
|
|
|
|
|
this._maskPostrender = null;
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// ✅ 关键:先清除所有图层的源,再移除图层,这能更有效地中断网络请求
|
|
|
|
|
|
const clearLayer = (layer: any) => {
|
|
|
|
|
|
if (layer && layer.getSource) {
|
|
|
|
|
|
const source = layer.getSource();
|
|
|
|
|
|
if (source && source.clear) {
|
|
|
|
|
|
source.clear(); // 清除缓存
|
2026-04-20 16:58:33 +08:00
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// 对于 WMTS/XYZ,OL 内部有 TileQueue,dispose 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);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
this.layerRegistry.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// 3. 清除点图层 Registry
|
2026-04-20 16:58:33 +08:00
|
|
|
|
if (this.pointLayerRegistry) {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
this.pointLayerRegistry.forEach(clearLayer);
|
2026-04-20 16:58:33 +08:00
|
|
|
|
this.pointLayerRegistry.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 清除地图实例
|
|
|
|
|
|
if (this.map) {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
// 先清除交互和覆盖物
|
2026-04-20 16:58:33 +08:00
|
|
|
|
this.map.getInteractions().clear();
|
|
|
|
|
|
this.map.getOverlays().clear();
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置 target 为 undefined 会从 DOM 移除 canvas
|
2026-04-22 17:53:20 +08:00
|
|
|
|
this.map.setTarget(undefined);
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 销毁地图实例
|
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;
|
2026-05-15 17:38:03 +08:00
|
|
|
|
this.geoJsonData = null;
|
|
|
|
|
|
this.geoJsonData1 = null;
|
2026-04-20 16:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('地图实例已销毁');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|