/** * 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 => { 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 => { 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 => { 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 };