503 lines
13 KiB
TypeScript
503 lines
13 KiB
TypeScript
/**
|
|
* OpenLayers 地图工具函数集合
|
|
* 提供地图要素查询、选择和处理功能
|
|
*
|
|
* @module mapQueryService
|
|
*/
|
|
|
|
import { GeoJSON } from "ol/format";
|
|
import { Feature } from "ol";
|
|
import { Point, LineString, Polygon } from "ol/geom";
|
|
import Geometry from "ol/geom/Geometry";
|
|
import TileState from "ol/TileState";
|
|
import { toLonLat } from "ol/proj";
|
|
import { booleanIntersects, buffer, point, toWgs84 } from "@turf/turf";
|
|
import config from "@config/config";
|
|
import RenderFeature from "ol/render/Feature";
|
|
import Map from "ol/Map";
|
|
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
|
import VectorTileSource from "ol/source/VectorTile";
|
|
import VectorLayer from "ol/layer/Vector";
|
|
|
|
// ========== 类型定义 ==========
|
|
|
|
/**
|
|
* 几何类型枚举
|
|
*/
|
|
enum GeometryType {
|
|
POINT = "Point",
|
|
LINE_STRING = "LineString",
|
|
POLYGON = "Polygon",
|
|
}
|
|
|
|
/**
|
|
* 地图点击事件
|
|
*/
|
|
interface MapClickEvent {
|
|
coordinate: number[];
|
|
}
|
|
|
|
// ========== 常量配置 ==========
|
|
|
|
/**
|
|
* GeoServer 服务配置获取函数
|
|
*/
|
|
const getGeoserverConfig = () => ({
|
|
url: config.MAP_URL,
|
|
workspace: config.MAP_WORKSPACE,
|
|
layers: ["geo_pipes_mat", "geo_junctions_mat", "geo_valves"],
|
|
wfsVersion: "1.0.0",
|
|
outputFormat: "application/json",
|
|
});
|
|
|
|
/**
|
|
* 地图交互配置
|
|
*/
|
|
const MAP_CONFIG = {
|
|
hitTolerance: 5, // 像素容差
|
|
bufferUnits: "meters" as const,
|
|
} as const;
|
|
|
|
// ========== 辅助函数 ==========
|
|
|
|
/**
|
|
* 将扁平坐标数组转换为点坐标
|
|
* @param flatCoordinates 扁平坐标数组
|
|
* @returns 点坐标 [x, y]
|
|
*/
|
|
const flatCoordinatesToPoint = (flatCoordinates: number[]): number[] => {
|
|
return [flatCoordinates[0], flatCoordinates[1]];
|
|
};
|
|
|
|
/**
|
|
* 将扁平坐标数组转换为线坐标
|
|
* @param flatCoordinates 扁平坐标数组
|
|
* @returns 线坐标数组 [[x1, y1], [x2, y2], ...]
|
|
*/
|
|
const flatCoordinatesToLineString = (flatCoordinates: number[]): number[][] => {
|
|
const lineCoords: number[][] = [];
|
|
for (let i = 0; i < flatCoordinates.length; i += 2) {
|
|
lineCoords.push([flatCoordinates[i], flatCoordinates[i + 1]]);
|
|
}
|
|
return lineCoords;
|
|
};
|
|
|
|
/**
|
|
* 将扁平坐标数组转换为多边形坐标
|
|
* @param flatCoordinates 扁平坐标数组
|
|
* @param ends 环的结束位置数组
|
|
* @returns 多边形坐标数组 [[[x1, y1], [x2, y2], ...]]
|
|
*/
|
|
const flatCoordinatesToPolygon = (
|
|
flatCoordinates: number[],
|
|
ends: number[]
|
|
): number[][][] => {
|
|
const rings: number[][][] = [];
|
|
let start = 0;
|
|
|
|
for (const end of ends) {
|
|
const ring: number[][] = [];
|
|
for (let i = start; i < end; i += 2) {
|
|
ring.push([flatCoordinates[i], flatCoordinates[i + 1]]);
|
|
}
|
|
rings.push(ring);
|
|
start = end;
|
|
}
|
|
return rings;
|
|
};
|
|
|
|
/**
|
|
* 将 RenderFeature 转换为标准 Feature
|
|
* @param renderFeature OpenLayers 渲染要素
|
|
* @returns 标准 Feature 对象或 undefined
|
|
*/
|
|
const convertRenderFeatureToFeature = (
|
|
renderFeature: RenderFeature
|
|
): Feature | undefined => {
|
|
if (!renderFeature) {
|
|
return undefined;
|
|
}
|
|
|
|
const geometry = renderFeature.getGeometry();
|
|
if (!geometry) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
let clonedGeometry: Geometry;
|
|
|
|
// 检查是否为标准几何体
|
|
if (geometry instanceof Geometry) {
|
|
clonedGeometry = geometry;
|
|
} else {
|
|
// 处理 RenderFeature 的几何体
|
|
const type = geometry.getType();
|
|
const flatCoordinates = geometry.getFlatCoordinates();
|
|
|
|
switch (type) {
|
|
case GeometryType.POINT:
|
|
clonedGeometry = new Point(flatCoordinatesToPoint(flatCoordinates));
|
|
break;
|
|
|
|
case GeometryType.LINE_STRING:
|
|
clonedGeometry = new LineString(
|
|
flatCoordinatesToLineString(flatCoordinates)
|
|
);
|
|
break;
|
|
|
|
case GeometryType.POLYGON:
|
|
const ends = (geometry as any).getEnds?.() || [
|
|
flatCoordinates.length,
|
|
];
|
|
clonedGeometry = new Polygon(
|
|
flatCoordinatesToPolygon(flatCoordinates, ends)
|
|
);
|
|
break;
|
|
|
|
default:
|
|
console.warn(`不支持的几何体类型: ${type}`);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return new Feature({
|
|
geometry: clonedGeometry,
|
|
...renderFeature.getProperties(),
|
|
});
|
|
} catch (error) {
|
|
console.error("RenderFeature 转换为 Feature 时出错:", error);
|
|
return undefined;
|
|
}
|
|
};
|
|
/**
|
|
* 构建 WFS 查询 URL
|
|
* @param layer 图层名称
|
|
* @param orFilter CQL 过滤条件
|
|
* @returns WFS 查询 URL
|
|
*/
|
|
const buildWfsUrl = (layer: string, orFilter: string): string => {
|
|
const { url, workspace, wfsVersion, outputFormat } = getGeoserverConfig();
|
|
const params = new URLSearchParams({
|
|
service: "WFS",
|
|
version: wfsVersion,
|
|
request: "GetFeature",
|
|
typeName: `${workspace}:${layer}`,
|
|
outputFormat: outputFormat,
|
|
CQL_FILTER: orFilter,
|
|
});
|
|
return `${url}/${workspace}/ows?${params.toString()}`;
|
|
};
|
|
|
|
/**
|
|
* 从指定图层查询要素
|
|
* @param layer 图层名称
|
|
* @param orFilter CQL 过滤条件
|
|
* @returns GeoJSON Feature 数组
|
|
*/
|
|
const fetchFeaturesFromLayer = async (
|
|
layer: string,
|
|
orFilter: string
|
|
): Promise<Feature[]> => {
|
|
try {
|
|
const url = buildWfsUrl(layer, orFilter);
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP 错误: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const json = await response.json();
|
|
return new GeoJSON().readFeatures(json);
|
|
} catch (error) {
|
|
console.error(`图层 ${layer} 查询失败:`, error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 根据 ID 列表通过 GeoServer WFS 服务查询要素
|
|
* @param ids 要素 ID 数组
|
|
* @param layer 可选的指定图层名称
|
|
* @returns Feature 数组
|
|
*/
|
|
const queryFeaturesByIds = async (
|
|
ids: string[],
|
|
layer?: string
|
|
): Promise<Feature[]> => {
|
|
if (!ids.length) {
|
|
return [];
|
|
}
|
|
|
|
const orFilter = ids.map((id) => `id='${id}'`).join(" OR ");
|
|
|
|
try {
|
|
if (!layer) {
|
|
// 查询所有配置的图层
|
|
const promises = getGeoserverConfig().layers.map((layerName) =>
|
|
fetchFeaturesFromLayer(layerName, orFilter)
|
|
);
|
|
|
|
const results = await Promise.all(promises);
|
|
return results.flat();
|
|
} else {
|
|
// 查询指定图层
|
|
return await fetchFeaturesFromLayer(layer, orFilter);
|
|
}
|
|
} catch (error) {
|
|
console.error("根据 IDs 查询要素时出错:", error);
|
|
return [];
|
|
}
|
|
};
|
|
/**
|
|
* 获取地图上所有 VectorTileSource
|
|
* @param map OpenLayers 地图对象
|
|
* @returns VectorTileSource 数组
|
|
*/
|
|
const getVectorTileSources = (map: Map): VectorTileSource[] => {
|
|
return map
|
|
.getAllLayers()
|
|
.filter((layer) => layer instanceof WebGLVectorTileLayer)
|
|
.map((layer) => layer.getSource() as VectorTileSource)
|
|
.filter((source) => source !== null);
|
|
};
|
|
|
|
/**
|
|
* 确保缩放级别在有效范围内
|
|
* @param z 原始缩放级别
|
|
* @param minZoom 最小缩放级别
|
|
* @param maxZoom 最大缩放级别
|
|
* @returns 调整后的缩放级别
|
|
*/
|
|
const clampZoomLevel = (
|
|
z: number,
|
|
minZoom: number,
|
|
maxZoom: number
|
|
): number => {
|
|
return Math.max(minZoom, Math.min(maxZoom, z));
|
|
};
|
|
|
|
/**
|
|
* 按几何类型对要素进行分类
|
|
* @param features 要素数组
|
|
* @returns 分类后的要素对象
|
|
*/
|
|
const classifyFeaturesByGeometry = (
|
|
features: Feature[]
|
|
): {
|
|
points: Feature[];
|
|
lines: Feature[];
|
|
others: Feature[];
|
|
} => {
|
|
const points: Feature[] = [];
|
|
const lines: Feature[] = [];
|
|
const others: Feature[] = [];
|
|
|
|
features.forEach((feature) => {
|
|
const geometryType = feature.getGeometry()?.getType();
|
|
switch (geometryType) {
|
|
case GeometryType.POINT:
|
|
points.push(feature);
|
|
break;
|
|
case GeometryType.LINE_STRING:
|
|
lines.push(feature);
|
|
break;
|
|
default:
|
|
others.push(feature);
|
|
}
|
|
});
|
|
|
|
return { points, lines, others };
|
|
};
|
|
|
|
/**
|
|
* 检查要素是否与缓冲区相交
|
|
* @param feature 要素
|
|
* @param buffered 缓冲区几何对象
|
|
* @returns 是否相交
|
|
*/
|
|
const isFeatureIntersectsBuffer = (
|
|
feature: Feature,
|
|
buffered: any
|
|
): boolean => {
|
|
if (!feature || !buffered) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const geoJSONGeometry = new GeoJSON().writeGeometryObject(
|
|
feature.getGeometry()!
|
|
);
|
|
const bufferedGeometry = buffered.geometry;
|
|
return booleanIntersects(toWgs84(geoJSONGeometry), bufferedGeometry);
|
|
} catch (error) {
|
|
console.error("要素相交检测失败:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 从 VectorTile 中提取选中的要素
|
|
* @param vectorTiles 矢量瓦片数组
|
|
* @param buffered 缓冲区几何对象
|
|
* @returns 选中的要素数组
|
|
*/
|
|
const extractSelectedFeatures = (
|
|
vectorTiles: any[],
|
|
buffered: any
|
|
): Feature[] => {
|
|
const allFeatures: Feature[] = [];
|
|
|
|
vectorTiles.forEach((vectorTile) => {
|
|
if (vectorTile.getState() !== TileState.LOADED) {
|
|
return;
|
|
}
|
|
|
|
const renderFeatures = vectorTile.getFeatures();
|
|
const selectedFeatures = renderFeatures
|
|
.map((renderFeature: RenderFeature) =>
|
|
convertRenderFeatureToFeature(renderFeature)
|
|
)
|
|
.filter(
|
|
(feature: Feature | undefined): feature is Feature =>
|
|
feature !== undefined && isFeatureIntersectsBuffer(feature, buffered)
|
|
);
|
|
|
|
allFeatures.push(...selectedFeatures);
|
|
});
|
|
|
|
return allFeatures;
|
|
};
|
|
|
|
/**
|
|
* 处理地图点击选择要素
|
|
* @param event 地图点击事件
|
|
* @param map OpenLayers 地图对象
|
|
* @returns 选中的第一个要素的 Promise,如果没有选中则返回 null
|
|
*/
|
|
const handleMapClickSelectFeatures = async (
|
|
event: MapClickEvent,
|
|
map: Map
|
|
): Promise<Feature | null> => {
|
|
if (!map) {
|
|
return null;
|
|
}
|
|
|
|
const coord = event.coordinate;
|
|
const view = map.getView();
|
|
const projection = view.getProjection();
|
|
const pixelRatio = window.devicePixelRatio;
|
|
|
|
// 获取缩放级别并确保为整数
|
|
let z = Math.floor(view.getZoom() || 0) - 1;
|
|
|
|
// 存储所有选中的要素
|
|
const allSelectedFeatures: Feature[] = [];
|
|
let isFromVectorLayer = false;
|
|
|
|
// 1. 优先处理 VectorLayer - 使用 forEachFeatureAtPixel
|
|
const pixel = map.getPixelFromCoordinate(coord);
|
|
map.forEachFeatureAtPixel(
|
|
pixel,
|
|
(feature) => {
|
|
// 只处理标准 Feature,排除 RenderFeature
|
|
// 使用更宽松的检查:检查是否有 getGeometry 方法且不是 RenderFeature
|
|
if (feature && !(feature instanceof RenderFeature)) {
|
|
allSelectedFeatures.push(feature as Feature);
|
|
isFromVectorLayer = true;
|
|
} else return false; // 继续遍历所有要素
|
|
},
|
|
{
|
|
hitTolerance: MAP_CONFIG.hitTolerance,
|
|
layerFilter: (layer) => layer instanceof VectorLayer,
|
|
}
|
|
);
|
|
|
|
// 2. 如果 VectorLayer 中没有找到要素,再处理 VectorTileSource
|
|
if (allSelectedFeatures.length === 0) {
|
|
const vectorTileSources = getVectorTileSources(map);
|
|
for (const vectorTileSource of vectorTileSources) {
|
|
const tileGrid = vectorTileSource.getTileGrid();
|
|
if (!tileGrid) {
|
|
continue;
|
|
}
|
|
|
|
// 调整缩放级别到有效范围
|
|
const minZoom = tileGrid.getMinZoom();
|
|
const maxZoom = tileGrid.getMaxZoom();
|
|
z = clampZoomLevel(z, minZoom, maxZoom);
|
|
|
|
// 获取瓦片坐标
|
|
const tileCoord = tileGrid.getTileCoordForCoordAndZ(coord, z);
|
|
const resolution = tileGrid.getResolution(tileCoord[0]);
|
|
|
|
// 创建点击点的缓冲区
|
|
const hitPoint = point(toLonLat(coord));
|
|
const buffered = buffer(hitPoint, resolution * MAP_CONFIG.hitTolerance, {
|
|
units: MAP_CONFIG.bufferUnits,
|
|
});
|
|
|
|
// 获取矢量瓦片
|
|
const vectorRenderTile = vectorTileSource.getTile(
|
|
tileCoord[0],
|
|
tileCoord[1],
|
|
tileCoord[2],
|
|
pixelRatio,
|
|
projection
|
|
);
|
|
|
|
const vectorTiles = vectorTileSource.getSourceTiles(
|
|
pixelRatio,
|
|
projection,
|
|
vectorRenderTile
|
|
);
|
|
|
|
// 提取选中的要素
|
|
const selectedFeatures = extractSelectedFeatures(vectorTiles, buffered);
|
|
allSelectedFeatures.push(...selectedFeatures);
|
|
}
|
|
}
|
|
|
|
// 按几何类型优先级排序:点 > 线 > 其他
|
|
const { points, lines, others } =
|
|
classifyFeaturesByGeometry(allSelectedFeatures);
|
|
const prioritizedFeatures = [
|
|
...points.reverse(),
|
|
...lines.reverse(),
|
|
...others.reverse(),
|
|
];
|
|
|
|
// 获取第一个要素
|
|
const firstFeature = prioritizedFeatures[0];
|
|
if (!firstFeature) {
|
|
return null;
|
|
}
|
|
|
|
// 如果要素来自 VectorLayer,直接返回,不需要通过 WFS 查询
|
|
if (isFromVectorLayer) {
|
|
return firstFeature;
|
|
}
|
|
|
|
// 如果要素来自 VectorTileSource,需要通过 WFS 查询完整信息
|
|
const queryId = firstFeature.getProperties().id;
|
|
const layerName = firstFeature.getProperties().layer;
|
|
if (layerName === "geo_pipes" || layerName === "geo_junctions") {
|
|
layerName.concat("_mat");
|
|
}
|
|
if (!queryId) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const features = await queryFeaturesByIds([queryId], layerName);
|
|
return features[0] || null;
|
|
} catch (error) {
|
|
console.error("查询要素详情失败:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// ========== 导出 ==========
|
|
|
|
export { handleMapClickSelectFeatures, queryFeaturesByIds };
|
|
export type { MapClickEvent };
|