完善爆管分析面板;整合地图查询函数
This commit is contained in:
@@ -1,438 +0,0 @@
|
||||
/**
|
||||
* OpenLayers 地图工具函数集合
|
||||
* 提供地图要素查询、选择和处理功能
|
||||
*/
|
||||
|
||||
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";
|
||||
// ========== 常量配置 ==========
|
||||
|
||||
const GEOSERVER_CONFIG = {
|
||||
url: config.GEOSERVER_URL,
|
||||
network: config.GEOSERVER_NETWORK,
|
||||
layers: ['geo_pipes_mat', 'geo_junctions_mat'],
|
||||
wfsVersion: '1.0.0',
|
||||
outputFormat: 'application/json',
|
||||
};
|
||||
|
||||
const MAP_CONFIG = {
|
||||
hitTolerance: 5, // 像素容差
|
||||
bufferUnits: 'meters',
|
||||
};
|
||||
|
||||
// ========== 几何类型枚举 ==========
|
||||
|
||||
const GEOMETRY_TYPES = {
|
||||
POINT: 'Point',
|
||||
LINE_STRING: 'LineString',
|
||||
POLYGON: 'Polygon',
|
||||
};
|
||||
|
||||
// ========== 工具函数 ==========
|
||||
|
||||
/**
|
||||
* 构建 WFS 查询 URL
|
||||
* @param {string} layer - 图层名称
|
||||
* @param {string} cqlFilter - CQL 过滤条件
|
||||
* @returns {string} 完整的 WFS 查询 URL
|
||||
*/
|
||||
const buildWfsUrl = (layer, cqlFilter) => {
|
||||
const { url, network, wfsVersion, outputFormat } = GEOSERVER_CONFIG;
|
||||
const params = new URLSearchParams({
|
||||
service: 'WFS',
|
||||
version: wfsVersion,
|
||||
request: 'GetFeature',
|
||||
typeName: `${network}:${layer}`,
|
||||
outputFormat,
|
||||
CQL_FILTER: cqlFilter,
|
||||
});
|
||||
return `${url}/${network}/ows?${params.toString()}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询单个图层的要素
|
||||
* @param {string} layer - 图层名称
|
||||
* @param {string} cqlFilter - CQL 过滤条件
|
||||
* @returns {Promise<Feature[]>} 要素数组
|
||||
*/
|
||||
const queryLayerFeatures = async (layer, cqlFilter) => {
|
||||
try {
|
||||
const url = buildWfsUrl(layer, cqlFilter);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
return new GeoJSON().readFeatures(json);
|
||||
} catch (error) {
|
||||
console.error(`图层 ${layer} 查询失败:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 IDs,通过 Geoserver WFS 服务查询要素
|
||||
* @param {string[]} ids - 要素 ID 数组
|
||||
* @param {string} [layer] - 可选的特定图层名称,不传则查询所有图层
|
||||
* @returns {Promise<Feature[]>} 查询到的要素数组
|
||||
*/
|
||||
export const queryFeaturesByIds = async (ids, layer = null) => {
|
||||
if (!ids || !ids.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cqlFilter = ids.map((id) => `id=${id}`).join(' OR ');
|
||||
|
||||
try {
|
||||
if (layer) {
|
||||
// 查询指定图层
|
||||
return await queryLayerFeatures(layer, cqlFilter);
|
||||
}
|
||||
|
||||
// 查询所有图层
|
||||
const { layers } = GEOSERVER_CONFIG;
|
||||
const promises = layers.map((layerName) =>
|
||||
queryLayerFeatures(layerName, cqlFilter)
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const features = results.flat();
|
||||
|
||||
return features;
|
||||
} catch (error) {
|
||||
console.error('根据 IDs 查询要素时出错:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将扁平坐标数组转换为坐标对数组
|
||||
* @param {number[]} flatCoordinates - 扁平坐标数组 [x1, y1, x2, y2, ...]
|
||||
* @returns {number[][]} 坐标对数组 [[x1, y1], [x2, y2], ...]
|
||||
*/
|
||||
const flatCoordinatesToPairs = (flatCoordinates) => {
|
||||
const pairs = [];
|
||||
for (let i = 0; i < flatCoordinates.length; i += 2) {
|
||||
pairs.push([flatCoordinates[i], flatCoordinates[i + 1]]);
|
||||
}
|
||||
return pairs;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建点几何对象
|
||||
* @param {number[]} flatCoordinates - 扁平坐标数组
|
||||
* @returns {Point} 点几何对象
|
||||
*/
|
||||
const createPointGeometry = (flatCoordinates) => {
|
||||
const coordinates = [flatCoordinates[0], flatCoordinates[1]];
|
||||
return new Point(coordinates);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建线几何对象
|
||||
* @param {number[]} flatCoordinates - 扁平坐标数组
|
||||
* @returns {LineString} 线几何对象
|
||||
*/
|
||||
const createLineStringGeometry = (flatCoordinates) => {
|
||||
const lineCoords = flatCoordinatesToPairs(flatCoordinates);
|
||||
return new LineString(lineCoords);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建面几何对象
|
||||
* @param {number[]} flatCoordinates - 扁平坐标数组
|
||||
* @param {Object} geometry - 原始几何对象
|
||||
* @returns {Polygon} 面几何对象
|
||||
*/
|
||||
const createPolygonGeometry = (flatCoordinates, geometry) => {
|
||||
// 获取环的结束位置
|
||||
const ends = geometry.getEnds ? geometry.getEnds() : [flatCoordinates.length];
|
||||
const rings = [];
|
||||
let start = 0;
|
||||
|
||||
for (const end of ends) {
|
||||
const ring = [];
|
||||
for (let i = start; i < end; i += 2) {
|
||||
ring.push([flatCoordinates[i], flatCoordinates[i + 1]]);
|
||||
}
|
||||
rings.push(ring);
|
||||
start = end;
|
||||
}
|
||||
|
||||
return new Polygon(rings);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 RenderFeature 转换为标准 Feature
|
||||
* @param {Object} renderFeature - 渲染要素对象
|
||||
* @returns {Feature|null} OpenLayers Feature 对象,转换失败返回 null
|
||||
*/
|
||||
const renderFeature2Feature = (renderFeature) => {
|
||||
if (!renderFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const geometry = renderFeature.getGeometry();
|
||||
if (!geometry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let clonedGeometry;
|
||||
|
||||
if (geometry instanceof Geometry) {
|
||||
// 标准 Feature 的几何体,直接使用
|
||||
clonedGeometry = geometry;
|
||||
} else {
|
||||
// RenderFeature 的几何体,需要转换
|
||||
const type = geometry.getType();
|
||||
const flatCoordinates = geometry.getFlatCoordinates();
|
||||
|
||||
switch (type) {
|
||||
case 'Point':
|
||||
clonedGeometry = createPointGeometry(flatCoordinates);
|
||||
break;
|
||||
|
||||
case 'LineString':
|
||||
clonedGeometry = createLineStringGeometry(flatCoordinates);
|
||||
break;
|
||||
|
||||
case 'Polygon':
|
||||
clonedGeometry = createPolygonGeometry(flatCoordinates, geometry);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('不支持的几何体类型:', type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的 Feature,包含几何体和属性
|
||||
const feature = new Feature({
|
||||
geometry: clonedGeometry,
|
||||
...renderFeature.getProperties(),
|
||||
});
|
||||
|
||||
return feature;
|
||||
} catch (error) {
|
||||
console.error('RenderFeature 转换 Feature 时出错:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 对要素按几何类型进行分类
|
||||
* @param {Feature} feature - OpenLayers 要素
|
||||
* @param {Object} categorized - 分类存储对象
|
||||
*/
|
||||
const categorizeFeatureByGeometry = (feature, categorized) => {
|
||||
const geometryType = feature.getGeometry()?.getType();
|
||||
|
||||
if (geometryType === GEOMETRY_TYPES.POINT) {
|
||||
categorized.points.push(feature);
|
||||
} else if (geometryType === GEOMETRY_TYPES.LINE_STRING) {
|
||||
categorized.lines.push(feature);
|
||||
} else {
|
||||
categorized.others.push(feature);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查要素是否在缓冲区内
|
||||
* @param {Feature} feature - OpenLayers 要素
|
||||
* @param {Object} bufferedGeometry - 缓冲区几何对象
|
||||
* @returns {boolean} 是否相交
|
||||
*/
|
||||
const isFeatureInBuffer = (feature, bufferedGeometry) => {
|
||||
if (!feature || !bufferedGeometry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const geoJSONGeometry = new GeoJSON().writeGeometryObject(
|
||||
feature.getGeometry()
|
||||
);
|
||||
return booleanIntersects(toWgs84(geoJSONGeometry), bufferedGeometry);
|
||||
} catch (error) {
|
||||
console.error('要素缓冲区检查失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理矢量瓦片,提取符合条件的要素
|
||||
* @param {Object} vectorTile - 矢量瓦片对象
|
||||
* @param {Object} buffered - 缓冲区对象
|
||||
* @param {Object} categorized - 分类存储对象
|
||||
*/
|
||||
const processVectorTile = (vectorTile, buffered, categorized) => {
|
||||
if (vectorTile.getState() !== TileState.LOADED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderFeatures = vectorTile.getFeatures();
|
||||
if (!renderFeatures || renderFeatures.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFeatures = renderFeatures
|
||||
.map((renderFeature) => renderFeature2Feature(renderFeature))
|
||||
.filter((feature) => feature !== null) // 过滤转换失败的要素
|
||||
.filter((feature) => isFeatureInBuffer(feature, buffered?.geometry));
|
||||
|
||||
selectedFeatures.forEach((feature) =>
|
||||
categorizeFeatureByGeometry(feature, categorized)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理矢量瓦片源,提取所有符合条件的要素
|
||||
* @param {Object} vectorTileSource - 矢量瓦片源
|
||||
* @param {number[]} coord - 坐标
|
||||
* @param {number} z - 缩放级别
|
||||
* @param {Object} projection - 投影
|
||||
* @param {Object} categorized - 分类存储对象
|
||||
*/
|
||||
const processVectorTileSource = (
|
||||
vectorTileSource,
|
||||
coord,
|
||||
z,
|
||||
projection,
|
||||
categorized
|
||||
) => {
|
||||
const tileGrid = vectorTileSource.getTileGrid();
|
||||
|
||||
if (!tileGrid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保缩放级别在有效范围内
|
||||
const minZoom = tileGrid.getMinZoom();
|
||||
const maxZoom = tileGrid.getMaxZoom();
|
||||
const validZ = Math.max(minZoom, Math.min(z, maxZoom));
|
||||
|
||||
const [x, y] = coord;
|
||||
const tileCoord = tileGrid.getTileCoordForCoordAndZ([x, y], validZ);
|
||||
const resolution = tileGrid.getResolution(tileCoord[0]);
|
||||
|
||||
// 创建缓冲区用于容差计算
|
||||
const { hitTolerance, bufferUnits } = MAP_CONFIG;
|
||||
const hitPoint = point(toLonLat(coord));
|
||||
const buffered = buffer(hitPoint, resolution * hitTolerance, {
|
||||
units: bufferUnits,
|
||||
});
|
||||
|
||||
// 获取矢量渲染瓦片
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const vectorRenderTile = vectorTileSource.getTile(
|
||||
tileCoord[0],
|
||||
tileCoord[1],
|
||||
tileCoord[2],
|
||||
pixelRatio,
|
||||
projection
|
||||
);
|
||||
// 检查 vectorRenderTile 是否有效
|
||||
if (!vectorRenderTile) {
|
||||
return;
|
||||
}
|
||||
// 获取源瓦片
|
||||
const vectorTiles = typeof vectorTileSource.getSourceTiles === 'function' ? vectorTileSource.getSourceTiles(
|
||||
pixelRatio,
|
||||
projection,
|
||||
vectorRenderTile
|
||||
) : [];
|
||||
|
||||
vectorTiles.forEach((vectorTile) =>
|
||||
processVectorTile(vectorTile, buffered, categorized)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理地图点击事件,选择要素
|
||||
* @param {Object} event - 地图点击事件
|
||||
* @param {Object} map - OpenLayers 地图对象
|
||||
* @param {Function} setHighlightFeature - 设置高亮要素的回调函数
|
||||
*/
|
||||
export const handleMapClickSelectFeatures = (
|
||||
event,
|
||||
map,
|
||||
setHighlightFeature
|
||||
) => {
|
||||
if (!map || !event?.coordinate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coord = event.coordinate;
|
||||
const view = map.getView();
|
||||
const currentZoom = view.getZoom() || 0;
|
||||
const z = Math.floor(currentZoom) - 1;
|
||||
const projection = view.getProjection();
|
||||
|
||||
// 获取所有矢量瓦片图层源
|
||||
const vectorTileSources = map
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.getSource && layer.getSource())
|
||||
.map((layer) => layer.getSource())
|
||||
.filter((source) => source && source.getTileGrid);
|
||||
|
||||
if (!vectorTileSources.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按几何类型分类存储要素
|
||||
const categorized = {
|
||||
points: [],
|
||||
lines: [],
|
||||
others: [],
|
||||
};
|
||||
|
||||
// 处理所有矢量瓦片源
|
||||
vectorTileSources.forEach((vectorTileSource) =>
|
||||
processVectorTileSource(vectorTileSource, coord, z, projection, categorized)
|
||||
);
|
||||
|
||||
// 按优先级合并要素:点 > 线 > 其他
|
||||
const selectedFeatures = [
|
||||
...categorized.points,
|
||||
...categorized.lines,
|
||||
...categorized.others,
|
||||
];
|
||||
|
||||
// 处理选中的第一个要素
|
||||
if (selectedFeatures.length > 0) {
|
||||
const firstFeature = selectedFeatures[0];
|
||||
const queryId = firstFeature?.getProperties()?.id;
|
||||
|
||||
if (queryId) {
|
||||
queryFeaturesByIds([queryId])
|
||||
.then((features) => {
|
||||
if (features && features.length > 0) {
|
||||
setHighlightFeature(features[0]);
|
||||
} else {
|
||||
setHighlightFeature(null);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('查询要素详情失败:', error);
|
||||
setHighlightFeature(null);
|
||||
});
|
||||
} else {
|
||||
setHighlightFeature(null);
|
||||
}
|
||||
} else {
|
||||
setHighlightFeature(null);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
handleMapClickSelectFeatures,
|
||||
queryFeaturesByIds,
|
||||
};
|
||||
471
src/utils/mapQueryService.ts
Normal file
471
src/utils/mapQueryService.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
// ========== 类型定义 ==========
|
||||
|
||||
/**
|
||||
* 几何类型枚举
|
||||
*/
|
||||
enum GeometryType {
|
||||
POINT = "Point",
|
||||
LINE_STRING = "LineString",
|
||||
POLYGON = "Polygon",
|
||||
}
|
||||
|
||||
/**
|
||||
* 地图点击事件
|
||||
*/
|
||||
interface MapClickEvent {
|
||||
coordinate: number[];
|
||||
}
|
||||
|
||||
// ========== 常量配置 ==========
|
||||
|
||||
/**
|
||||
* GeoServer 服务配置
|
||||
*/
|
||||
const GEOSERVER_CONFIG = {
|
||||
url: config.mapUrl,
|
||||
workspace: "TJWater",
|
||||
layers: ["geo_pipes_mat", "geo_junctions_mat"],
|
||||
wfsVersion: "1.0.0",
|
||||
outputFormat: "application/json",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 地图交互配置
|
||||
*/
|
||||
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 } = GEOSERVER_CONFIG;
|
||||
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 = GEOSERVER_CONFIG.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;
|
||||
|
||||
// 获取所有 VectorTileSource
|
||||
const vectorTileSources = getVectorTileSources(map);
|
||||
if (!vectorTileSources.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 存储所有选中的要素
|
||||
const allSelectedFeatures: Feature[] = [];
|
||||
|
||||
// 遍历所有 VectorTileSource
|
||||
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, ...lines, ...others];
|
||||
|
||||
// 获取第一个要素的 ID 并查询完整信息
|
||||
const firstFeature = prioritizedFeatures[0];
|
||||
if (!firstFeature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const queryId = firstFeature.getProperties().id;
|
||||
if (!queryId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const features = await queryFeaturesByIds([queryId]);
|
||||
return features[0] || null;
|
||||
} catch (error) {
|
||||
console.error("查询要素详情失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 导出 ==========
|
||||
|
||||
export { handleMapClickSelectFeatures, queryFeaturesByIds };
|
||||
export type { MapClickEvent };
|
||||
Reference in New Issue
Block a user