Files
TJWaterServer/src/utils/mapQueryService.ts

503 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };