1245 lines
39 KiB
TypeScript
1245 lines
39 KiB
TypeScript
|
|
import { MapInterface } from './map';
|
|||
|
|
import OlMap from 'ol/Map';
|
|||
|
|
import View from 'ol/View';
|
|||
|
|
import TileLayer from 'ol/layer/Tile';
|
|||
|
|
import VectorLayer from 'ol/layer/Vector';
|
|||
|
|
import VectorSource from 'ol/source/Vector';
|
|||
|
|
import GeoJSON from 'ol/format/GeoJSON';
|
|||
|
|
import Style from 'ol/style/Style';
|
|||
|
|
import Fill from 'ol/style/Fill';
|
|||
|
|
import Stroke from 'ol/style/Stroke';
|
|||
|
|
// ✅ 新增导入
|
|||
|
|
import Icon from 'ol/style/Icon';
|
|||
|
|
import Text from 'ol/style/Text';
|
|||
|
|
|
|||
|
|
import LayerGroup from 'ol/layer/Group';
|
|||
|
|
import OSM from 'ol/source/OSM';
|
|||
|
|
import WMTS from 'ol/source/WMTS';
|
|||
|
|
import WMTSTileGrid from 'ol/tilegrid/WMTS';
|
|||
|
|
import { get as getProjection, fromLonLat, toLonLat } from 'ol/proj';
|
|||
|
|
import {
|
|||
|
|
defaults as defaultInteractions,
|
|||
|
|
Draw,
|
|||
|
|
DoubleClickZoom
|
|||
|
|
} from 'ol/interaction';
|
|||
|
|
import { getTopLeft, getWidth } from 'ol/extent';
|
|||
|
|
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
|||
|
|
import { servers } from './mapurlManage';
|
|||
|
|
import geoJsonData from '@/assets/geoJson.json';
|
|||
|
|
import geoJsonData1 from '@/assets/geoJson1.json';
|
|||
|
|
import { XYZ } from 'ol/source';
|
|||
|
|
import Feature from 'ol/Feature';
|
|||
|
|
import Point from 'ol/geom/Point'; // ✅ 新增导入
|
|||
|
|
import LineString from 'ol/geom/LineString';
|
|||
|
|
import Polygon from 'ol/geom/Polygon';
|
|||
|
|
import Overlay from 'ol/Overlay';
|
|||
|
|
import {
|
|||
|
|
getLength as getSphericalLength,
|
|||
|
|
getArea as getSphericalArea
|
|||
|
|
} from 'ol/sphere';
|
|||
|
|
import { unByKey } from 'ol/Observable';
|
|||
|
|
import { MDOptions } from './map.class';
|
|||
|
|
import { getIconPath } from '@/utils/index'; // ✅ 确保引入获取图标路径的工具函数
|
|||
|
|
|
|||
|
|
// 定义与 leaflet 中相同的常量
|
|||
|
|
const CENTER_positionCN = [114.17112499999996, 38]; // OpenLayers 使用 [lon, lat]
|
|||
|
|
const MIN_ZOOM = 4.23;
|
|||
|
|
const MAX_ZOOM = 22;
|
|||
|
|
const INITIAL_ZOOM = 4.5;
|
|||
|
|
|
|||
|
|
// 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标)
|
|||
|
|
const BOUNDS_SW = [26.5, -9.99999999999929];
|
|||
|
|
const BOUNDS_NE = [180.00000000000074, 60.06349386538693];
|
|||
|
|
|
|||
|
|
export class MapOl implements MapInterface {
|
|||
|
|
map: OlMap | null = null;
|
|||
|
|
view: View | null = null;
|
|||
|
|
// ✅ 新增:存储 key -> layer 实例,用于控制显示隐藏和切换
|
|||
|
|
private layerRegistry: Map<string, any> = new Map();
|
|||
|
|
// ✅ 新增:记录当前正在显示的“切换 Key”,用于判断是否需要重新加载或只是切换显隐
|
|||
|
|
private baseLayerConfig: TileLayer | null = null;
|
|||
|
|
private hydropBaseConfig: TileLayer | null = null;
|
|||
|
|
private REGISTRY_KEY = 'customBaseLayer';
|
|||
|
|
|
|||
|
|
// ✅ 新增:量算相关属性
|
|||
|
|
private drawInteraction: any | null = null;
|
|||
|
|
private measureLayer: VectorLayer | null = null;
|
|||
|
|
private measureSource: VectorSource | null = null;
|
|||
|
|
// ✅ 新增:存储点图层的 Registry,key 为 layerType,value 为 VectorLayer
|
|||
|
|
private pointLayerRegistry: Map<string, VectorLayer> = new Map();
|
|||
|
|
|
|||
|
|
private BASEID: string = '01';
|
|||
|
|
|
|||
|
|
//地图初始化
|
|||
|
|
init(container: HTMLElement, rectangle?: any): Promise<any> {
|
|||
|
|
try {
|
|||
|
|
console.log('OL init初始化container', container);
|
|||
|
|
|
|||
|
|
// 1. 将经纬度边界转换为 Web Mercator (EPSG:3857) 坐标,用于限制视图范围
|
|||
|
|
const minCoordinate = fromLonLat(BOUNDS_SW);
|
|||
|
|
const maxCoordinate = fromLonLat(BOUNDS_NE);
|
|||
|
|
const extent = [
|
|||
|
|
minCoordinate[0],
|
|||
|
|
minCoordinate[1],
|
|||
|
|
maxCoordinate[0],
|
|||
|
|
maxCoordinate[1]
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 2. 创建视图 (View)
|
|||
|
|
this.view = new View({
|
|||
|
|
center: fromLonLat(CENTER_positionCN), // 设置中心点
|
|||
|
|
zoom: INITIAL_ZOOM,
|
|||
|
|
minZoom: MIN_ZOOM,
|
|||
|
|
maxZoom: MAX_ZOOM,
|
|||
|
|
constrainOnlyCenter: false, // 允许部分视图超出边界,但中心点受限(类似 Leaflet 默认行为)
|
|||
|
|
extent: extent, // 限制视图范围
|
|||
|
|
smoothExtentConstraint: false // 平滑边界约束
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ✅ 3. 自定义鼠标滚轮交互
|
|||
|
|
// Leaflet 的 wheelPxPerZoomLevel: 180 意味着缩放比较“慢/平滑”。
|
|||
|
|
// OpenLayers 默认每次滚动 delta 为 1/3 级。
|
|||
|
|
// 我们可以减小 delta 值来让缩放更平滑(需要更多滚动次数才能变一级),
|
|||
|
|
// 或者使用 constrainResolution: false 允许非整数缩放,获得更丝滑的效果。
|
|||
|
|
const mouseWheelInteraction = new MouseWheelZoom({
|
|||
|
|
duration: 100, // 缩放动画持续时间 (ms),Leaflet 默认也有动画
|
|||
|
|
maxDelta: 0.5,
|
|||
|
|
constrainResolution: false // ✅ 关键:false 允许缩放到非整数级别 (如 4.5, 4.6),实现平滑缩放
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 4. 创建地图实例
|
|||
|
|
this.map = new OlMap({
|
|||
|
|
target: container,
|
|||
|
|
layers: [],
|
|||
|
|
view: this.view,
|
|||
|
|
controls: [], // 对应 Leaflet 的 attributionControl: false, zoomControl: false
|
|||
|
|
interactions: defaultInteractions({
|
|||
|
|
doubleClickZoom: true,
|
|||
|
|
dragPan: true,
|
|||
|
|
|
|||
|
|
pinchRotate: false // 通常禁用旋转,除非需要
|
|||
|
|
}).extend([mouseWheelInteraction])
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 5. 监听缩放结束事件 (对应 Leaflet 的 'zoomend')
|
|||
|
|
this.view.on('change:resolution', () => {
|
|||
|
|
// OpenLayers 没有直接的 'zoomend' 事件,通常监听 resolution 变化
|
|||
|
|
// 如果需要防抖处理,可以添加 debounce
|
|||
|
|
console.log('当前缩放级别', this.view?.getZoom());
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ✅ 新增:监听鼠标移动,动态改变光标样式
|
|||
|
|
this.map.on('pointermove', evt => {
|
|||
|
|
const pixel = evt.pixel;
|
|||
|
|
// 检测鼠标下方是否有矢量要素
|
|||
|
|
const feature = this.map?.forEachFeatureAtPixel(pixel, feature => {
|
|||
|
|
return feature;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const targetElement = this.map?.getTargetElement() as HTMLElement;
|
|||
|
|
|
|||
|
|
if (targetElement) {
|
|||
|
|
if (feature) {
|
|||
|
|
// 如果鼠标下有要素,显示手型
|
|||
|
|
targetElement.style.cursor = 'pointer';
|
|||
|
|
} else {
|
|||
|
|
// 否则恢复默认
|
|||
|
|
targetElement.style.cursor = '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return Promise.resolve(this.map);
|
|||
|
|
} catch (e) {
|
|||
|
|
console.log('OL Init Error', e);
|
|||
|
|
return Promise.reject(e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化加载描点数据
|
|||
|
|
* @param pointData 图层配置对象或数据数组
|
|||
|
|
* @param layerType 图层类型/Key
|
|||
|
|
* @param mdoptions 描点选项配置
|
|||
|
|
*/
|
|||
|
|
addInitDataLayer(
|
|||
|
|
pointData: any,
|
|||
|
|
layerType: any,
|
|||
|
|
mdoptions?: MDOptions
|
|||
|
|
): void {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
|
|||
|
|
let dataArray: any[] = [];
|
|||
|
|
let targetLayerKey: string = layerType;
|
|||
|
|
|
|||
|
|
// 1. 参数解析
|
|||
|
|
if (Array.isArray(pointData)) {
|
|||
|
|
dataArray = pointData;
|
|||
|
|
} else {
|
|||
|
|
dataArray = pointData.data || [];
|
|||
|
|
targetLayerKey = pointData.key || layerType;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!targetLayerKey) {
|
|||
|
|
console.warn('缺少图层 Key,无法加载描点');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 获取或创建 VectorLayer
|
|||
|
|
let vectorLayer = this.pointLayerRegistry.get(targetLayerKey);
|
|||
|
|
let vectorSource: VectorSource;
|
|||
|
|
|
|||
|
|
const shouldClear = mdoptions?.isRemove !== false;
|
|||
|
|
|
|||
|
|
if (!vectorLayer) {
|
|||
|
|
vectorSource = new VectorSource();
|
|||
|
|
vectorLayer = new VectorLayer({
|
|||
|
|
source: vectorSource,
|
|||
|
|
zIndex: 100, // 确保点在底图之上
|
|||
|
|
declutter: true,
|
|||
|
|
style: (feature: any) => this.createPointStyle(feature) // 使用动态样式函数
|
|||
|
|
});
|
|||
|
|
this.pointLayerRegistry.set(targetLayerKey, vectorLayer);
|
|||
|
|
this.map.addLayer(vectorLayer);
|
|||
|
|
} else {
|
|||
|
|
vectorSource = vectorLayer.getSource() as VectorSource;
|
|||
|
|
if (shouldClear) {
|
|||
|
|
vectorSource.clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (dataArray.length === 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
`开始加载 OL 图层 [${targetLayerKey}] 的描点,数量: ${dataArray.length}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 3. 遍历数据生成 Feature
|
|||
|
|
const features: Feature[] = [];
|
|||
|
|
|
|||
|
|
dataArray.forEach((item: any) => {
|
|||
|
|
const {
|
|||
|
|
lgtd,
|
|||
|
|
lttd,
|
|||
|
|
stcd,
|
|||
|
|
stnm,
|
|||
|
|
iconCode,
|
|||
|
|
anchoPointState,
|
|||
|
|
titleName,
|
|||
|
|
ennm
|
|||
|
|
} = item;
|
|||
|
|
|
|||
|
|
if (lgtd == null || lttd == null) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 确定图标和文字
|
|||
|
|
let iconUrl = '';
|
|||
|
|
|
|||
|
|
// 获取图标 URL (参考 Leaflet 逻辑)
|
|||
|
|
if (iconCode) {
|
|||
|
|
iconUrl = getIconPath(iconCode);
|
|||
|
|
}
|
|||
|
|
// 如果没有 iconCode,可以根据 anchoPointState 或其他逻辑获取,这里暂略
|
|||
|
|
|
|||
|
|
if (!iconUrl) {
|
|||
|
|
// 默认图标,防止报错
|
|||
|
|
iconUrl = getIconPath('default') || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示的文字优先使用 titleName,其次 ennm,最后 stnm
|
|||
|
|
const labelText = titleName || ennm || stnm || '';
|
|||
|
|
|
|||
|
|
// 5. 创建 Feature
|
|||
|
|
const feature = new Feature({
|
|||
|
|
geometry: new Point(fromLonLat([lgtd, lttd])), // ✅ 关键:坐标转换 EPSG:4326 -> EPSG:3857
|
|||
|
|
...item, // 保留原始数据以便点击事件使用
|
|||
|
|
_layerKey: targetLayerKey
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 将自定义属性绑定到 feature,以便样式函数访问
|
|||
|
|
feature.set('_iconUrl', iconUrl);
|
|||
|
|
feature.set('_labelText', labelText);
|
|||
|
|
|
|||
|
|
features.push(feature);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 6. 批量添加要素
|
|||
|
|
if (features.length > 0) {
|
|||
|
|
vectorSource.addFeatures(features);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`图层 [${targetLayerKey}] 描点加载完成`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建点样式 (模拟 Leaflet 的 DivIcon 效果,支持随缩放动态调整大小)
|
|||
|
|
*/
|
|||
|
|
private createPointStyle(feature: Feature): Style {
|
|||
|
|
const iconUrl = feature.get('_iconUrl') as string;
|
|||
|
|
const labelText = feature.get('_labelText') as string;
|
|||
|
|
|
|||
|
|
// 如果没图标,返回空样式
|
|||
|
|
if (!iconUrl) {
|
|||
|
|
return new Style();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 1. 获取当前地图分辨率
|
|||
|
|
// 注意:如果样式函数在地图未完全初始化时调用,view 可能为 null
|
|||
|
|
const currentResolution = this.view ? this.view.getResolution() : 1000;
|
|||
|
|
|
|||
|
|
// ✅ 2. 定义基准分辨率和基准缩放比例
|
|||
|
|
// INITIAL_ZOOM (4.5) 对应的分辨率大约是 3000-4000 左右 (取决于投影)
|
|||
|
|
// 这里我们用一个经验公式:分辨率越小(zoom越大),scale 越大
|
|||
|
|
// 假设在初始分辨率下 scale 为 0.7
|
|||
|
|
const baseResolution = 3000; // 这是一个估算值,你可以根据实际效果微调
|
|||
|
|
const baseScale = 0.7;
|
|||
|
|
|
|||
|
|
// 计算动态缩放比例:
|
|||
|
|
// 如果 currentResolution 变小 (放大地图),ratio 变大 -> icon 变大
|
|||
|
|
// 限制最大和最小缩放,防止过大或过小
|
|||
|
|
// let dynamicScale = baseScale * (baseResolution / (currentResolution || 1));
|
|||
|
|
const currentZoom = this.view ? this.view.getZoom() : 4.5;
|
|||
|
|
let dynamicScale = 0.7 + (currentZoom - 4.5) * 0.08;
|
|||
|
|
|
|||
|
|
// 限制范围:最小 0.5,最大 3.0 (可根据需求调整)
|
|||
|
|
dynamicScale = Math.max(0.5, Math.min(3.0, dynamicScale));
|
|||
|
|
|
|||
|
|
// ✅ 3. 动态字体大小
|
|||
|
|
// 基础字体 12px,随缩放比例线性变化
|
|||
|
|
const fontSize = Math.max(10, Math.min(24, 12 * dynamicScale));
|
|||
|
|
|
|||
|
|
return new Style({
|
|||
|
|
image: new Icon({
|
|||
|
|
src: iconUrl,
|
|||
|
|
scale: dynamicScale, // ✅ 使用动态缩放
|
|||
|
|
anchor: [0.5, 0.5], // 锚点:水平居中,底部对齐
|
|||
|
|
// 确保图标清晰
|
|||
|
|
crossOrigin: 'anonymous'
|
|||
|
|
}),
|
|||
|
|
text: new Text({
|
|||
|
|
text: labelText,
|
|||
|
|
offsetY: -20 * dynamicScale, // ✅ 偏移量也随缩放调整,保持相对位置
|
|||
|
|
font: `${fontSize}px sans-serif`, // ✅ 使用动态字体大小
|
|||
|
|
fill: new Fill({ color: '#fff' }),
|
|||
|
|
stroke: new Stroke({ color: 'rgba(0, 0, 0, .9)', width: 2 }),
|
|||
|
|
textAlign: 'center',
|
|||
|
|
declutterMode: 'declutter'
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 初始化加载基础图层
|
|||
|
|
* @param layer 图层配置对象
|
|||
|
|
*/
|
|||
|
|
addBaseDataLayer(layer: any, isCache: boolean = false): void {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
|
|||
|
|
console.log('OL addBaseDataLayer', layer);
|
|||
|
|
|
|||
|
|
if (layer.type === 'wmts') {
|
|||
|
|
if (!layer.url) return;
|
|||
|
|
let url = !isCache ? layer.url : layer.url_3d;
|
|||
|
|
const urlParams = new URLSearchParams(url.split('?')[1]);
|
|||
|
|
if (layer.key === this.REGISTRY_KEY) {
|
|||
|
|
this.baseLayerConfig = layer;
|
|||
|
|
}
|
|||
|
|
// 1. 解析参数
|
|||
|
|
// 注意:如果 layer.url 已经是完整 URL,可能需要调整解析逻辑。
|
|||
|
|
// 这里假设 layer.url 包含查询参数,或者我们直接使用硬编码的配置来确保正确性
|
|||
|
|
const layerName: string = urlParams.get('LAYER') || ''; // 根据你的 Leaflet 代码硬编码或从 layer 对象获取
|
|||
|
|
const matrixSetName: any = urlParams.get('TILEMATRIXSET'); // TileMatrixSet 名称
|
|||
|
|
|
|||
|
|
// 2. 获取标准 EPSG:3857 投影信息
|
|||
|
|
const projection = getProjection('EPSG:3857');
|
|||
|
|
if (!projection) {
|
|||
|
|
console.error('无法获取 EPSG:3857 投影');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const projectionExtent = projection.getExtent();
|
|||
|
|
|
|||
|
|
// 3. 计算分辨率和矩阵ID
|
|||
|
|
// GeoServer 的 TileMatrixSet 通常基于标准 Web Mercator,但 ID 格式可能不同
|
|||
|
|
const size = getWidth(projectionExtent) / 256;
|
|||
|
|
const maxZoom = 13; // 根据 Leaflet 中的 maxNativeZoom: 12 或 13 设定
|
|||
|
|
|
|||
|
|
const resolutions = new Array(maxZoom + 1);
|
|||
|
|
const matrixIds = new Array(maxZoom + 1);
|
|||
|
|
|
|||
|
|
for (let z = 0; z <= maxZoom; ++z) {
|
|||
|
|
// 生成标准分辨率
|
|||
|
|
resolutions[z] = size / Math.pow(2, z);
|
|||
|
|
|
|||
|
|
// 【关键修复】:构造符合 GeoServer 要求的 Matrix ID
|
|||
|
|
// 通常格式为: "TileMatrixSetName:ZoomLevel" 或 "EPSG:3857:ZoomLevel"
|
|||
|
|
// 根据你的 URL 推测,ID 可能是 "EPSG:3857_qgc_qsj_arcgistiles_l13:0"
|
|||
|
|
matrixIds[z] = `${matrixSetName}:${z}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('Using Matrix IDs sample:', matrixIds[0], matrixIds[1]);
|
|||
|
|
|
|||
|
|
// 4. 创建 WMTS 图层
|
|||
|
|
const wmtsLayer = new TileLayer({
|
|||
|
|
source: new WMTS({
|
|||
|
|
url: layer.url.split('?')[0], // 使用代理路径
|
|||
|
|
layer: layerName,
|
|||
|
|
matrixSet: matrixSetName,
|
|||
|
|
format: 'image/png',
|
|||
|
|
projection: projection, // ✅ 使用标准的 EPSG:3857 投影对象
|
|||
|
|
tileGrid: new WMTSTileGrid({
|
|||
|
|
origin: getTopLeft(projectionExtent), // ✅ 使用标准投影的左上角原点
|
|||
|
|
resolutions: resolutions,
|
|||
|
|
matrixIds: matrixIds // ✅ 使用上面生成的带前缀的 IDs
|
|||
|
|
}),
|
|||
|
|
style: 'default',
|
|||
|
|
wrapX: true, // 允许水平方向上的循环
|
|||
|
|
crossOrigin: 'anonymous' // 允许跨域
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (layer.key === this.REGISTRY_KEY) {
|
|||
|
|
!isCache ? wmtsLayer.setZIndex(-100) : wmtsLayer.setZIndex(-99);
|
|||
|
|
} else {
|
|||
|
|
wmtsLayer.setZIndex(-99);
|
|||
|
|
}
|
|||
|
|
// 5. 注册与添加
|
|||
|
|
this.layerRegistry.set(layer.key, wmtsLayer);
|
|||
|
|
this.map.addLayer(wmtsLayer);
|
|||
|
|
layer._layer = wmtsLayer;
|
|||
|
|
} else if (layer.type == 'raster-dem') {
|
|||
|
|
const tileLayer = new TileLayer({
|
|||
|
|
source: new XYZ({
|
|||
|
|
url: layer.url,
|
|||
|
|
wrapX: true,
|
|||
|
|
crossOrigin: 'anonymous'
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
tileLayer.setZIndex(-100);
|
|||
|
|
this.layerRegistry.set(this.REGISTRY_KEY, tileLayer);
|
|||
|
|
this.map.addLayer(tileLayer);
|
|||
|
|
} else if (layer.type === 'vector') {
|
|||
|
|
if (layer.key === 'hydropBase') {
|
|||
|
|
this.hydropBaseConfig = layer;
|
|||
|
|
}
|
|||
|
|
// ✅ 1. 创建矢量源,关键是要配置投影转换
|
|||
|
|
const vectorSource = new VectorSource({
|
|||
|
|
features: new GeoJSON().readFeatures(geoJsonData1, {
|
|||
|
|
dataProjection: 'EPSG:4326',
|
|||
|
|
featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影
|
|||
|
|
})
|
|||
|
|
// url: layer.geojson_url +`&BASEID=BASEID'${this.BASEID}'` , // 远程 GeoJSON 地址
|
|||
|
|
// format: new GeoJSON({
|
|||
|
|
// featureProjection: 'EPSG:3857'
|
|||
|
|
// })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ✅ 2. 创建矢量图层,添加默认样式防止透明
|
|||
|
|
const vectorLayer = new VectorLayer({
|
|||
|
|
source: vectorSource,
|
|||
|
|
style: new Style({
|
|||
|
|
stroke: new Stroke({
|
|||
|
|
color: '#3399CC', // 默认蓝色边框
|
|||
|
|
width: 2
|
|||
|
|
}),
|
|||
|
|
fill: new Fill({
|
|||
|
|
color: 'rgba(51, 153, 204, 0.4)' // 默认半透明填充
|
|||
|
|
})
|
|||
|
|
}),
|
|||
|
|
// ✅ 3. 调整 zIndex,确保在底图之上可见
|
|||
|
|
zIndex: 100,
|
|||
|
|
visible: true
|
|||
|
|
});
|
|||
|
|
// 监听数据加载完成事件
|
|||
|
|
// vectorSource.on('featuresloadend', () => {
|
|||
|
|
// // 获取所有加载的要素
|
|||
|
|
// const features = vectorSource.getFeatures();
|
|||
|
|
// const feature = features[0];
|
|||
|
|
// const geometry = feature.getGeometry();
|
|||
|
|
// console.log(geometry)
|
|||
|
|
// // this.addClipToRasterLayer(this.layerRegistry.get(this.REGISTRY_KEY), geometry);
|
|||
|
|
// // this.applyMapMask(geometry);
|
|||
|
|
// });
|
|||
|
|
// 4. 注册与添加
|
|||
|
|
this.layerRegistry.set(layer.key, vectorLayer);
|
|||
|
|
this.map.addLayer(vectorLayer);
|
|||
|
|
layer._layer = vectorLayer;
|
|||
|
|
|
|||
|
|
console.log(`矢量图层 [${layer.key}] 已加载: ${layer.url}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 为栅格图层添加裁切效果
|
|||
|
|
* @param layer 栅格图层(TileLayer)
|
|||
|
|
* @param clipGeometry 裁切几何(Polygon 或 MultiPolygon,EPSG:3857 投影)
|
|||
|
|
*/
|
|||
|
|
private addClipToRasterLayer(layer: TileLayer<any>, clipGeometry: Geometry): void {
|
|||
|
|
if (!layer || !clipGeometry) return;
|
|||
|
|
|
|||
|
|
// 处理几何类型,统一为多边形数组
|
|||
|
|
let polygons: Polygon[] = [];
|
|||
|
|
const type = clipGeometry.getType();
|
|||
|
|
|
|||
|
|
if (type === 'Polygon') {
|
|||
|
|
polygons = [clipGeometry as Polygon];
|
|||
|
|
} else if (type === 'MultiPolygon') {
|
|||
|
|
const multiPolygon = clipGeometry as MultiPolygon;
|
|||
|
|
const coords = multiPolygon.getCoordinates();
|
|||
|
|
polygons = coords.map(coord => new Polygon(coord));
|
|||
|
|
} else {
|
|||
|
|
console.error('不支持的几何类型:', type);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 预存储所有外环坐标
|
|||
|
|
const allRings: number[][][] = [];
|
|||
|
|
for (const polygon of polygons) {
|
|||
|
|
const coords = polygon.getCoordinates();
|
|||
|
|
if (coords && coords[0] && coords[0].length > 0) {
|
|||
|
|
allRings.push(coords[0]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除旧事件
|
|||
|
|
layer.removeEventListener('prerender');
|
|||
|
|
layer.removeEventListener('postrender');
|
|||
|
|
|
|||
|
|
// 渲染前:设置裁切区域
|
|||
|
|
layer.on('prerender', (event) => {
|
|||
|
|
const context = event.context;
|
|||
|
|
const frameState = event.frameState;
|
|||
|
|
|
|||
|
|
if (!context || !frameState) return;
|
|||
|
|
|
|||
|
|
// 获取坐标转像素函数
|
|||
|
|
const toPixel = frameState.coordinateToPixel;
|
|||
|
|
if (!toPixel) return;
|
|||
|
|
|
|||
|
|
context.save();
|
|||
|
|
context.beginPath();
|
|||
|
|
|
|||
|
|
let hasPath = false;
|
|||
|
|
|
|||
|
|
for (const ring of allRings) {
|
|||
|
|
if (!ring || ring.length === 0) continue;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < ring.length; i++) {
|
|||
|
|
const coord = ring[i];
|
|||
|
|
const pixel = toPixel(coord);
|
|||
|
|
|
|||
|
|
if (!pixel || pixel.length < 2) continue;
|
|||
|
|
|
|||
|
|
if (i === 0) {
|
|||
|
|
context.moveTo(pixel[0], pixel[1]);
|
|||
|
|
} else {
|
|||
|
|
context.lineTo(pixel[0], pixel[1]);
|
|||
|
|
}
|
|||
|
|
hasPath = true;
|
|||
|
|
}
|
|||
|
|
context.closePath();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (hasPath) {
|
|||
|
|
context.clip();
|
|||
|
|
} else {
|
|||
|
|
context.restore();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 渲染后:恢复状态
|
|||
|
|
layer.on('postrender', (event) => {
|
|||
|
|
if (event.context) {
|
|||
|
|
event.context.restore();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 控制特定区域图层的显示(互斥显示)
|
|||
|
|
* @param regionId 区域ID (例如 "hebei", "13", "01" 等,需与图层 Key 或属性对应)
|
|||
|
|
* @param isAll 是否显示所有 (true: 显示所有图层; false: 仅显示匹配 regionId 的图层,隐藏其他)
|
|||
|
|
*/
|
|||
|
|
jdPanelControlShowAndHidden(regionId: string, isAll: boolean): void {
|
|||
|
|
this.BASEID = regionId;
|
|||
|
|
console.log(this.layerRegistry);
|
|||
|
|
console.log(this.layerRegistry.keys());
|
|||
|
|
// this.addBaseDataLayer(this.hydropBaseConfig);
|
|||
|
|
// this.controlBaseLayerTreeShowAndHidden(this.REGISTRY_KEY,this.REGISTRY_KEY,false)
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 基础图层显示影隐藏方法
|
|||
|
|
* @param layerType 图层类型 (备用,优先使用 key)
|
|||
|
|
* @param key 图层唯一标识 Key
|
|||
|
|
* @param checked true 为显示,false 为隐藏
|
|||
|
|
*/
|
|||
|
|
controlBaseLayerTreeShowAndHidden(
|
|||
|
|
layerType: String,
|
|||
|
|
key: String,
|
|||
|
|
checked: boolean
|
|||
|
|
): void {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
|
|||
|
|
// 1. 确定查找的 Key:优先使用传入的 key,如果为空则尝试使用 layerType
|
|||
|
|
const registryKey = key || layerType;
|
|||
|
|
|
|||
|
|
if (!registryKey) {
|
|||
|
|
console.warn(
|
|||
|
|
'controlBaseLayerTreeShowAndHidden: 缺少有效的 Key 或 LayerType'
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 2. 从注册表中获取图层实例
|
|||
|
|
const layerInstance = this.layerRegistry.get(registryKey as string);
|
|||
|
|
|
|||
|
|
if (layerInstance) {
|
|||
|
|
// 3. 设置可见性
|
|||
|
|
// OpenLayers 的 Layer.setVisible(boolean) 方法
|
|||
|
|
layerInstance.setVisible(checked);
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
`图层 [${registryKey}] 已设置为: ${checked ? '显示' : '隐藏'}`
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
console.warn(
|
|||
|
|
`未找到标识为 [${registryKey}] 的图层实例,当前注册表 keys:`,
|
|||
|
|
Array.from(this.layerRegistry.keys())
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 图层树控制描点数据显示隐藏方法
|
|||
|
|
mdLayerTreeShowOrHidden(layerType: String, checked?: boolean): void {
|
|||
|
|
const layerInstance = this.pointLayerRegistry.get(layerType as string);
|
|||
|
|
layerInstance.setVisible(checked);
|
|||
|
|
// this.service.mdLayerTreeShowOrHidden(layerType, checked)
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 缩放地图
|
|||
|
|
* @param type 'out' 缩小, 'in' 放大
|
|||
|
|
*/
|
|||
|
|
zoomToggle(type: 'out' | 'in'): void {
|
|||
|
|
if (!this.map || !this.view) return;
|
|||
|
|
|
|||
|
|
const currentZoom = this.view.getZoom();
|
|||
|
|
if (currentZoom === undefined) return;
|
|||
|
|
|
|||
|
|
// 定义缩放步长,对应 Leaflet 的 zoomDelta
|
|||
|
|
const zoomDelta = 1;
|
|||
|
|
|
|||
|
|
let targetZoom = currentZoom;
|
|||
|
|
|
|||
|
|
if (type === 'in') {
|
|||
|
|
targetZoom = currentZoom + zoomDelta;
|
|||
|
|
// 限制最大缩放级别
|
|||
|
|
if (targetZoom > MAX_ZOOM) {
|
|||
|
|
targetZoom = MAX_ZOOM;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
targetZoom = currentZoom - zoomDelta;
|
|||
|
|
// 限制最小缩放级别
|
|||
|
|
if (targetZoom < MIN_ZOOM) {
|
|||
|
|
targetZoom = MIN_ZOOM;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用 animate 实现平滑缩放动画
|
|||
|
|
this.view.animate({
|
|||
|
|
zoom: targetZoom,
|
|||
|
|
duration: 250, // 动画持续时间 (ms),与滚轮缩放保持一致
|
|||
|
|
easing: t => t // 线性缓动,也可以引入 ol/easing 使用更复杂的曲线
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 切换底图
|
|||
|
|
* @param key 业务标识 Key (如 's_province_boundaries', 'BASEMAP-white', 'BASEMAP-img')
|
|||
|
|
*/
|
|||
|
|
baseLayerSwitcher(key: string): void {
|
|||
|
|
if (!this.map || !this.view) return;
|
|||
|
|
|
|||
|
|
console.log('OL 切换底图 key:', key);
|
|||
|
|
|
|||
|
|
// 1. 如果注册表中已存在 "customBaseLayer",先将其从地图中移除
|
|||
|
|
const oldLayer = this.layerRegistry.get(this.REGISTRY_KEY);
|
|||
|
|
if (oldLayer) {
|
|||
|
|
this.map.removeLayer(oldLayer);
|
|||
|
|
this.layerRegistry.delete(this.REGISTRY_KEY);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 根据传入的业务 key 创建新的图层实例
|
|||
|
|
if (key == 's_province_boundaries') {
|
|||
|
|
this.addBaseDataLayer(this.baseLayerConfig);
|
|||
|
|
} else {
|
|||
|
|
this.addBaseDataLayer(this.baseLayerConfig, true);
|
|||
|
|
this.addBaseDataLayer(servers.BaseLayer[key]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 添加梯级流域图 (Vector Layer) - 使用本地 GeoJSON 数据
|
|||
|
|
* @param layer 图层配置对象
|
|||
|
|
* @param fillcolor 填充颜色
|
|||
|
|
* @param outlineColor 边框颜色
|
|||
|
|
* @param datas 需要显示的 RVCD 列表 (例如: ['SJLY25', ...]),如果为空则显示所有或根据业务逻辑处理
|
|||
|
|
*/
|
|||
|
|
addTertiarybasinLayer(
|
|||
|
|
layer: any,
|
|||
|
|
fillcolor: any,
|
|||
|
|
outlineColor: any,
|
|||
|
|
datas: any
|
|||
|
|
): void {
|
|||
|
|
if (!this.map || !this.view) return;
|
|||
|
|
|
|||
|
|
// 1. 检查是否已存在该图层,避免重复添加
|
|||
|
|
const existingLayer = this.layerRegistry.get(layer.key);
|
|||
|
|
if (existingLayer) {
|
|||
|
|
this.map.removeLayer(existingLayer);
|
|||
|
|
this.layerRegistry.delete(layer.key);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 创建样式函数
|
|||
|
|
const visibleStyle = new Style({
|
|||
|
|
fill: new Fill({ color: fillcolor }),
|
|||
|
|
stroke: new Stroke({ color: outlineColor, width: 1 })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const hiddenStyle = new Style({
|
|||
|
|
fill: new Fill({ color: 'rgba(0,0,0,0)' }),
|
|||
|
|
stroke: new Stroke({ color: 'rgba(0,0,0,0)' })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3. 创建矢量源 (VectorSource) 并直接加载数据
|
|||
|
|
const vectorSource = new VectorSource({
|
|||
|
|
features: new GeoJSON().readFeatures(geoJsonData, {
|
|||
|
|
featureProjection: 'EPSG:3857' // 确保转换到地图使用的投影
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 4. 数据过滤逻辑
|
|||
|
|
const allowedIds = Array.isArray(datas) ? datas : [];
|
|||
|
|
const features = vectorSource.getFeatures();
|
|||
|
|
|
|||
|
|
features.forEach(feature => {
|
|||
|
|
// 获取属性中的 RVCD
|
|||
|
|
const rvcd = feature.get('RVCD');
|
|||
|
|
|
|||
|
|
let isVisible = false;
|
|||
|
|
|
|||
|
|
// 逻辑:如果 datas 为空,通常意味着显示所有(或者都不显示,视具体需求而定)
|
|||
|
|
// 这里假设:datas 有值时严格过滤;datas 为空时显示所有
|
|||
|
|
if (allowedIds.length > 0) {
|
|||
|
|
isVisible = rvcd && allowedIds.includes(rvcd);
|
|||
|
|
} else {
|
|||
|
|
isVisible = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用样式
|
|||
|
|
feature.setStyle(isVisible ? visibleStyle : hiddenStyle);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 5. 创建矢量图层 (VectorLayer)
|
|||
|
|
const vectorLayer = new VectorLayer({
|
|||
|
|
source: vectorSource,
|
|||
|
|
style: null, // 样式已应用在 Feature 上,这里设为 null 或保留默认
|
|||
|
|
zIndex: 100,
|
|||
|
|
visible: layer.visible !== false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 6. 注册并添加到地图
|
|||
|
|
this.layerRegistry.set(layer.key, vectorLayer);
|
|||
|
|
this.map.addLayer(vectorLayer);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 移除梯级流域图
|
|||
|
|
* @param layer 图层配置对象 (需包含 key 属性)
|
|||
|
|
*/
|
|||
|
|
removeTertiarybasinLayer(layer: any): void {
|
|||
|
|
if (!this.map || !layer || !layer.key) {
|
|||
|
|
console.warn('removeTertiarybasinLayer: 无效的图层或地图未初始化');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 1. 从注册表中获取图层实例
|
|||
|
|
const layerInstance = this.layerRegistry.get(layer.key);
|
|||
|
|
|
|||
|
|
if (layerInstance) {
|
|||
|
|
// 2. 从地图中移除图层
|
|||
|
|
this.map.removeLayer(layerInstance);
|
|||
|
|
// 3. 从注册表中删除引用
|
|||
|
|
this.layerRegistry.delete(layer.key);
|
|||
|
|
|
|||
|
|
console.log(`梯级流域图层 [${layer.key}] 已移除`);
|
|||
|
|
} else {
|
|||
|
|
console.warn(`未找到标识为 [${layer.key}] 的梯级流域图层实例`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 地图打印/导出
|
|||
|
|
* 将当前地图视图导出为 PNG 图片,背景强制为白色
|
|||
|
|
*/
|
|||
|
|
mapOutPut(): void {
|
|||
|
|
if (!this.map) {
|
|||
|
|
console.warn('地图未初始化,无法打印');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('开始生成地图打印图片...');
|
|||
|
|
|
|||
|
|
// 1. 获取地图容器中的 canvas 元素
|
|||
|
|
const mapElement = this.map.getTargetElement();
|
|||
|
|
const canvas = mapElement.querySelector('canvas');
|
|||
|
|
|
|||
|
|
if (!canvas) {
|
|||
|
|
console.error('未找到地图 Canvas 元素,无法导出');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 2. 创建一个离屏 Canvas 用于合成白色背景
|
|||
|
|
const width = canvas.width;
|
|||
|
|
const height = canvas.height;
|
|||
|
|
|
|||
|
|
const offscreenCanvas = document.createElement('canvas');
|
|||
|
|
offscreenCanvas.width = width;
|
|||
|
|
offscreenCanvas.height = height;
|
|||
|
|
const ctx = offscreenCanvas.getContext('2d');
|
|||
|
|
|
|||
|
|
if (!ctx) {
|
|||
|
|
throw new Error('无法获取 Canvas 上下文');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 填充白色背景
|
|||
|
|
ctx.fillStyle = '#FFFFFF';
|
|||
|
|
ctx.fillRect(0, 0, width, height);
|
|||
|
|
|
|||
|
|
// 4. 将原地图 Canvas 绘制到离屏 Canvas 上
|
|||
|
|
ctx.drawImage(canvas, 0, 0);
|
|||
|
|
|
|||
|
|
// 5. 将合成后的 Canvas 转换为 Data URL
|
|||
|
|
const dataURL = offscreenCanvas.toDataURL('image/png');
|
|||
|
|
|
|||
|
|
// 6. 创建临时链接并触发下载
|
|||
|
|
const link = document.createElement('a');
|
|||
|
|
link.download = `map_export_${new Date().getTime()}.png`;
|
|||
|
|
link.href = dataURL;
|
|||
|
|
|
|||
|
|
// 模拟点击
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
|
|||
|
|
console.log('地图图片已生成并触发下载(背景已白化)');
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('地图导出失败', e);
|
|||
|
|
alert('地图导出失败,请检查控制台错误信息。');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 切换双击缩放交互的状态
|
|||
|
|
*/
|
|||
|
|
private toggleDoubleClickZoom(active: boolean): void {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
this.map.getInteractions().forEach(interaction => {
|
|||
|
|
if (interaction instanceof DoubleClickZoom) {
|
|||
|
|
interaction.setActive(active);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建并返回测量 Tooltip Overlay
|
|||
|
|
*/
|
|||
|
|
private createMeasureTooltipOverlay(): Overlay {
|
|||
|
|
const element = document.createElement('div');
|
|||
|
|
element.className = 'measure-tooltip tooltip-measure';
|
|||
|
|
Object.assign(element.style, {
|
|||
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|||
|
|
color: 'white',
|
|||
|
|
padding: '4px 8px',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
fontSize: '12px',
|
|||
|
|
whiteSpace: 'nowrap'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return new Overlay({
|
|||
|
|
element,
|
|||
|
|
offset: [0, -15],
|
|||
|
|
positioning: 'bottom-center'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 通用测量启动方法
|
|||
|
|
* @param geomType 'LineString' | 'Polygon'
|
|||
|
|
* @param drawStyle 绘制中的样式函数
|
|||
|
|
* @param finishedStyle 完成后的样式
|
|||
|
|
* @param onResult 计算结果的回调 (feature, resultText)
|
|||
|
|
*/
|
|||
|
|
private startMeasurement(
|
|||
|
|
geomType: 'LineString' | 'Polygon',
|
|||
|
|
drawStyle: any,
|
|||
|
|
finishedStyle: Style,
|
|||
|
|
onResult: (feature: Feature, text: string) => void
|
|||
|
|
): void {
|
|||
|
|
if (!this.map || !this.view) return;
|
|||
|
|
|
|||
|
|
console.log(`启动${geomType === 'LineString' ? '长度' : '面积'}量算模式`);
|
|||
|
|
|
|||
|
|
// 1. 初始化图层
|
|||
|
|
if (!this.measureLayer) {
|
|||
|
|
this.measureSource = new VectorSource();
|
|||
|
|
this.measureLayer = new VectorLayer({
|
|||
|
|
source: this.measureSource,
|
|||
|
|
zIndex: 1000
|
|||
|
|
});
|
|||
|
|
this.map.addLayer(this.measureLayer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 清理旧交互
|
|||
|
|
if (this.drawInteraction) {
|
|||
|
|
this.map.removeInteraction(this.drawInteraction);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 禁用双击缩放
|
|||
|
|
this.toggleDoubleClickZoom(false);
|
|||
|
|
|
|||
|
|
// 3. 创建绘制交互
|
|||
|
|
this.drawInteraction = new Draw({
|
|||
|
|
source: this.measureSource,
|
|||
|
|
type: geomType,
|
|||
|
|
style: drawStyle
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.map.addInteraction(this.drawInteraction);
|
|||
|
|
|
|||
|
|
let changeListenerKey: any;
|
|||
|
|
let currentTooltip: Overlay | null = null;
|
|||
|
|
|
|||
|
|
// 4. 监听绘制开始
|
|||
|
|
this.drawInteraction.on('drawstart', (evt: any) => {
|
|||
|
|
const feature = evt.feature;
|
|||
|
|
const geom = feature.getGeometry();
|
|||
|
|
|
|||
|
|
// 创建动态 Tooltip
|
|||
|
|
currentTooltip = this.createMeasureTooltipOverlay();
|
|||
|
|
this.map?.addOverlay(currentTooltip);
|
|||
|
|
|
|||
|
|
// 监听几何变化更新 Tooltip
|
|||
|
|
changeListenerKey = geom!.on('change', () => {
|
|||
|
|
if (currentTooltip && currentTooltip.getElement() && geom) {
|
|||
|
|
// 根据类型计算临时值
|
|||
|
|
let tempText = '';
|
|||
|
|
if (geomType === 'LineString') {
|
|||
|
|
tempText = this.formatLength(geom as LineString);
|
|||
|
|
} else {
|
|||
|
|
tempText = this.formatArea(
|
|||
|
|
geom as import('ol/geom/Polygon').default
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
currentTooltip.getElement()!.innerHTML = tempText;
|
|||
|
|
|
|||
|
|
// 对于多边形,Tooltip 跟随鼠标最后一个点;对于线,也是最后一个点
|
|||
|
|
const lastCoord = (geom as any).getLastCoordinate();
|
|||
|
|
if (lastCoord) currentTooltip.setPosition(lastCoord);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 5. 监听绘制结束
|
|||
|
|
this.drawInteraction.on('drawend', (evt: any) => {
|
|||
|
|
const feature = evt.feature;
|
|||
|
|
const geom = feature.getGeometry();
|
|||
|
|
|
|||
|
|
// ✅ 修复:检查几何体是否有效
|
|||
|
|
// 对于 Polygon,如果坐标点数不足(例如只点了两下),geom 可能是无效的或面积为0
|
|||
|
|
if (!geom || geom.getCoordinates().length === 0) {
|
|||
|
|
console.warn('无效的几何体,已忽略');
|
|||
|
|
this.measureSource?.removeFeature(feature);
|
|||
|
|
if (currentTooltip) this.map?.removeOverlay(currentTooltip);
|
|||
|
|
if (changeListenerKey) unByKey(changeListenerKey);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理监听和临时 Tooltip
|
|||
|
|
if (changeListenerKey) unByKey(changeListenerKey);
|
|||
|
|
if (currentTooltip) this.map?.removeOverlay(currentTooltip);
|
|||
|
|
|
|||
|
|
// ✅ 修复:强制设置样式,确保即使默认样式有问题也能显示
|
|||
|
|
feature.setStyle(finishedStyle);
|
|||
|
|
|
|||
|
|
// 计算最终结果并回调
|
|||
|
|
let resultText = '';
|
|||
|
|
let isValidResult = true;
|
|||
|
|
|
|||
|
|
if (geomType === 'LineString') {
|
|||
|
|
resultText = this.formatLength(geom as LineString);
|
|||
|
|
} else {
|
|||
|
|
// 对于多边形,再次检查面积是否过小或无效
|
|||
|
|
const polyGeom = geom as import('ol/geom/Polygon').default;
|
|||
|
|
const area = getSphericalArea(polyGeom);
|
|||
|
|
if (area < 0.0001) {
|
|||
|
|
// 极小面积视为无效绘制
|
|||
|
|
isValidResult = false;
|
|||
|
|
console.warn('面积过小,已忽略');
|
|||
|
|
this.measureSource?.removeFeature(feature);
|
|||
|
|
} else {
|
|||
|
|
resultText = this.formatArea(polyGeom);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只有结果有效才添加标签
|
|||
|
|
if (isValidResult) {
|
|||
|
|
onResult(feature, resultText);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理交互
|
|||
|
|
this.map?.removeInteraction(this.drawInteraction);
|
|||
|
|
this.drawInteraction = null;
|
|||
|
|
|
|||
|
|
// 恢复双击缩放
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.toggleDoubleClickZoom(true);
|
|||
|
|
}, 0);
|
|||
|
|
|
|||
|
|
console.log('测量结束');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 长度量算
|
|||
|
|
*/
|
|||
|
|
lengthCalculate(): void {
|
|||
|
|
this.startMeasurement(
|
|||
|
|
'LineString',
|
|||
|
|
(feature: any) => {
|
|||
|
|
// 绘制中只显示线,隐藏点
|
|||
|
|
return feature.getGeometry()?.getType() === 'LineString'
|
|||
|
|
? new Style({
|
|||
|
|
stroke: new Stroke({
|
|||
|
|
color: '#ffcc33',
|
|||
|
|
lineDash: [10, 10],
|
|||
|
|
width: 2
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
: [];
|
|||
|
|
},
|
|||
|
|
new Style({
|
|||
|
|
stroke: new Stroke({ color: '#9AD8E7', width: 3 }) // 加粗一点
|
|||
|
|
}),
|
|||
|
|
(feature, text) => {
|
|||
|
|
this.addFixedLabel(feature, text);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 面积量算
|
|||
|
|
*/
|
|||
|
|
areCalculate(): void {
|
|||
|
|
this.startMeasurement(
|
|||
|
|
'Polygon',
|
|||
|
|
(feature: any) => {
|
|||
|
|
// 绘制中显示线和填充
|
|||
|
|
return feature.getGeometry()?.getType() === 'Polygon'
|
|||
|
|
? new Style({
|
|||
|
|
stroke: new Stroke({
|
|||
|
|
color: '#ffcc33',
|
|||
|
|
lineDash: [10, 10],
|
|||
|
|
width: 2
|
|||
|
|
}),
|
|||
|
|
fill: new Fill({ color: 'rgba(255, 204, 51, 0.3)' }) // 提高透明度以便看清
|
|||
|
|
})
|
|||
|
|
: [];
|
|||
|
|
},
|
|||
|
|
new Style({
|
|||
|
|
stroke: new Stroke({ color: '#9AD8E7', width: 3 }), // 加粗边框
|
|||
|
|
fill: new Fill({ color: 'rgba(154, 216, 231, 0.4)' }) // 提高填充可见度
|
|||
|
|
}),
|
|||
|
|
(feature, text) => {
|
|||
|
|
this.addFixedLabel(feature, text);
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 移除量算结果
|
|||
|
|
*/
|
|||
|
|
removeQueryLayer(): void {
|
|||
|
|
// 1. 清除矢量数据 (线条)
|
|||
|
|
if (this.measureSource) {
|
|||
|
|
this.measureSource.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 清除所有相关 Overlay (Label)
|
|||
|
|
if (this.map) {
|
|||
|
|
// ✅ 修复:先获取所有 Overlay 的副本,避免在遍历过程中修改原数组导致漏删
|
|||
|
|
const overlaysToRemove: Overlay[] = [];
|
|||
|
|
|
|||
|
|
this.map.getOverlays().forEach(overlay => {
|
|||
|
|
const el = overlay.getElement();
|
|||
|
|
// 通过类名判断是否为我们创建的测量标签
|
|||
|
|
if (
|
|||
|
|
el &&
|
|||
|
|
(el.classList.contains('measure-tooltip') ||
|
|||
|
|
el.classList.contains('measure-label-container'))
|
|||
|
|
) {
|
|||
|
|
overlaysToRemove.push(overlay);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 统一移除
|
|||
|
|
overlaysToRemove.forEach(overlay => {
|
|||
|
|
this.map?.removeOverlay(overlay);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 确保恢复双击缩放
|
|||
|
|
this.toggleDoubleClickZoom(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 清除交互
|
|||
|
|
if (this.drawInteraction) {
|
|||
|
|
this.map?.removeInteraction(this.drawInteraction);
|
|||
|
|
this.drawInteraction = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('已清除所有量算结果');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加固定的 Label (带删除按钮)
|
|||
|
|
*/
|
|||
|
|
private addFixedLabel(feature: Feature, lengthText: string): void {
|
|||
|
|
if (!this.map) return;
|
|||
|
|
|
|||
|
|
const container = document.createElement('div');
|
|||
|
|
container.className = 'measure-label-container';
|
|||
|
|
|
|||
|
|
// 使用 Object.assign 批量设置样式,代码更整洁
|
|||
|
|
Object.assign(container.style, {
|
|||
|
|
position: 'absolute',
|
|||
|
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|||
|
|
border: '1px solid #000',
|
|||
|
|
padding: '4px 8px',
|
|||
|
|
borderRadius: '4px',
|
|||
|
|
fontSize: '12px',
|
|||
|
|
color: '#000',
|
|||
|
|
cursor: 'default',
|
|||
|
|
whiteSpace: 'nowrap',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
zIndex: '1001',
|
|||
|
|
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|||
|
|
pointerEvents: 'auto' // 确保可以点击
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const textSpan = document.createElement('span');
|
|||
|
|
textSpan.innerText = lengthText;
|
|||
|
|
|
|||
|
|
const closeBtn = document.createElement('span');
|
|||
|
|
closeBtn.innerHTML = '×';
|
|||
|
|
Object.assign(closeBtn.style, {
|
|||
|
|
fontWeight: 'bold',
|
|||
|
|
marginLeft: '5px',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
lineHeight: '1'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const geom: any = feature.getGeometry();
|
|||
|
|
let center: any;
|
|||
|
|
|
|||
|
|
// ✅ 修复:根据几何类型选择标签位置
|
|||
|
|
if (geom?.getType() === 'Polygon') {
|
|||
|
|
// 多边形使用内部点,确保标签在面内
|
|||
|
|
center = (geom as import('ol/geom/Polygon').default)
|
|||
|
|
.getInteriorPoint()
|
|||
|
|
.getCoordinates();
|
|||
|
|
} else {
|
|||
|
|
// 线使用中点
|
|||
|
|
center = (geom as LineString).getCoordinateAt(0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const overlay = new Overlay({
|
|||
|
|
element: container,
|
|||
|
|
positioning: 'top-center',
|
|||
|
|
offset: [0, -10],
|
|||
|
|
position: center
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
closeBtn.onclick = e => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
this.measureSource?.removeFeature(feature);
|
|||
|
|
this.map?.removeOverlay(overlay);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
container.appendChild(textSpan);
|
|||
|
|
container.appendChild(closeBtn);
|
|||
|
|
this.map.addOverlay(overlay);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化长度输出 (米)
|
|||
|
|
*/
|
|||
|
|
private formatLength(line: LineString): string {
|
|||
|
|
return '长度:' + getSphericalLength(line).toFixed(3) + 'm';
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 格式化面积输出 (km²)
|
|||
|
|
*/
|
|||
|
|
private formatArea(polygon: import('ol/geom/Polygon').default): string {
|
|||
|
|
// 获取球面面积 (平方米)
|
|||
|
|
const areaSqMeters = getSphericalArea(polygon);
|
|||
|
|
// 转换为平方公里
|
|||
|
|
const areaSqKm = areaSqMeters / 1000000;
|
|||
|
|
return '面积:' + areaSqKm.toFixed(3) + 'km²';
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 移除地图对象,释放资源
|
|||
|
|
*/
|
|||
|
|
destroy(): void {
|
|||
|
|
console.log('销毁地图实例...');
|
|||
|
|
|
|||
|
|
// 1. 清除量算相关资源
|
|||
|
|
this.removeQueryLayer();
|
|||
|
|
|
|||
|
|
// 2. 清除所有通过 layerRegistry 管理的图层
|
|||
|
|
if (this.layerRegistry) {
|
|||
|
|
this.layerRegistry.forEach((layer, key) => {
|
|||
|
|
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
|||
|
|
this.map.removeLayer(layer);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
this.layerRegistry.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 3. 清除点图层 Registry
|
|||
|
|
if (this.pointLayerRegistry) {
|
|||
|
|
this.pointLayerRegistry.forEach((layer, key) => {
|
|||
|
|
if (this.map && this.map.getLayers().getArray().includes(layer)) {
|
|||
|
|
this.map.removeLayer(layer);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
this.pointLayerRegistry.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 清除地图实例
|
|||
|
|
if (this.map) {
|
|||
|
|
this.map.getInteractions().clear();
|
|||
|
|
this.map.getOverlays().clear();
|
|||
|
|
this.map.setTarget(null);
|
|||
|
|
this.map.dispose();
|
|||
|
|
this.map = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 清除视图引用
|
|||
|
|
if (this.view) {
|
|||
|
|
this.view.dispose();
|
|||
|
|
this.view = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 6. 清除其他引用
|
|||
|
|
this.baseLayerConfig = null;
|
|||
|
|
this.measureSource = null;
|
|||
|
|
this.measureLayer = null;
|
|||
|
|
this.drawInteraction = null;
|
|||
|
|
|
|||
|
|
console.log('地图实例已销毁');
|
|||
|
|
}
|
|||
|
|
// ... 其他 MapInterface 方法待实现
|
|||
|
|
}
|