WFS (Web Feature Service) 查询完整知识点
一、WFS 核心概念
| 概念 | 说明 |
|---|---|
| WFS | OGC 标准服务,用于返回矢量要素数据(GeoJSON/GML),而非图片 |
| WMS vs WFS | WMS 返回图片(显示用),WFS 返回数据(查询/分析用) |
| GeoJSON | WFS 最常用的输出格式,Cesium 可直接加载 |
| GML | WFS 默认输出格式(XML 结构),体积大,不推荐前端使用 |
二、WFS 核心操作
| 操作 | 说明 | 是否必需 |
|---|---|---|
| GetCapabilities | 获取服务的元信息(支持的操作、图层、输出格式等) | ✅ 必需 |
| DescribeFeatureType | 获取要素类型的字段结构(列名、类型) | ✅ 必需 |
| GetFeature | 查询并返回要素数据(核心操作) | ✅ 必需 |
| Transaction | 创建、更新、删除要素(WFS-T 事务操作) | ❌ 可选 |
三、WFS 请求参数详解 (GetFeature)
const params = {
// 基础参数
service: 'WFS', // 固定值,服务类型
version: '1.1.0', // WFS 版本(1.0.0 / 1.1.0 / 2.0.0)
request: 'GetFeature', // 请求类型
// 图层参数
typeName: 'workspace:layer', // 工作区:图层名
// 输出参数
outputFormat: 'json', // 输出格式(json / application/json / gml)
srsName: 'EPSG:4326', // 坐标系
// 过滤参数
CQL_FILTER: "...", // CQL 过滤条件
bbox: "...", // 矩形过滤(与 CQL_FILTER 互斥!)
propertyName: "id,name", // 指定返回的字段
// 分页参数
maxFeatures: 100, // 最大返回数量
startIndex: 0, // 起始索引(分页用)
sortBy: "id ASC", // 排序
};
四、WFS 版本对比
| 特性 | WFS 1.0.0 | WFS 1.1.0 | WFS 2.0.0 |
|---|---|---|---|
| 默认输出格式 | GML2 | GML3 | GML3.2 |
| GeoJSON 支持 | 需插件 | 需插件 | 需插件 |
| 坐标顺序 | (经度, 纬度) | (纬度, 经度) ⚠️ | 可配置 |
| BBOX 参数 | 支持 | 支持 | 支持 |
| CQL_FILTER | 支持(GeoServer 扩展) | 支持 | 支持 |
| 分页参数 | maxFeatures | maxFeatures | count + startIndex |
⚠️ 重要:WFS 1.1.0 的坐标顺序是 (纬度, 经度),这是最常见的坑!
五、CQL 空间查询函数
| 函数 | 语法 | 说明 | 适用场景 |
|---|---|---|---|
| BBOX | BBOX(geom, minY, minX, maxY, maxX) | 矩形范围查询 | 框选查询(推荐) |
| INTERSECTS | INTERSECTS(geom, POLYGON(...)) | 相交查询 | 精确空间相交 |
| DWITHIN | DWITHIN(geom, POINT(...), distance, units) | 距离范围内查询 | 圆形范围、附近查询 |
| CONTAINS | CONTAINS(geom, POINT(...)) | 包含查询 | 判断点是否在多边形内 |
| WITHIN | WITHIN(geom, POLYGON(...)) | 被包含查询 | 判断要素是否在范围内 |
| TOUCHES | TOUCHES(geom1, geom2) | 边界接触 | 相邻要素查询 |
六、WFS 请求完整示例
// URL 方式
const url = '/geoserver/jiesen/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=jiesen:tb_list&outputFormat=json';
// 代码方式
const params = new URLSearchParams();
params.append('service', 'WFS');
params.append('version', '1.1.0');
params.append('request', 'GetFeature');
params.append('typeName', 'jiesen:tb_list');
params.append('outputFormat', 'json');
示例 2:属性过滤(CQL)
// 过滤:task_id 等于某个值,且 del_flag = 0
const cql = "task_id='fc9cb975-8570-4249-9454-3703c54bb65f' AND del_flag = 0";
params.append('CQL_FILTER', cql);
示例 3:空间过滤(BBOX - 正确坐标顺序)
// ⚠️ 注意:BBOX 参数顺序是 (纬度, 经度)
const bboxCondition = `BBOX(tb_shape, ${minLat}, ${minLon}, ${maxLat}, ${maxLon})`;
params.append('CQL_FILTER', bboxCondition);
示例 4:混合过滤(属性 + 空间)
const cqlFilter = "task_id='xxx' AND del_flag = 0";
const bboxCondition = `BBOX(tb_shape, ${minLat}, ${minLon}, ${maxLat}, ${maxLon})`;
params.append('CQL_FILTER', `${cqlFilter} AND ${bboxCondition}`);
示例 5:完整 WFS 查询代码
async function queryWFS(minLon, minLat, maxLon, maxLat, taskId) {
const wfsUrl = '/geoserver/jiesen/wfs';
const params = new URLSearchParams();
params.append('service', 'WFS');
params.append('version', '1.1.0');
params.append('request', 'GetFeature');
params.append('typeName', 'jiesen:tb_list');
params.append('outputFormat', 'json');
params.append('srsName', 'EPSG:4326');
params.append('CQL_FILTER', `task_id='${taskId}' AND BBOX(tb_shape, ${minLat}, ${minLon}, ${maxLat}, ${maxLon})`);
const response = await fetch(`${wfsUrl}?${params.toString()}`);
const geojson = await response.json();
return geojson;
}
七、常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Illegal property name: geom | 几何字段名错误 | 使用正确的字段名(如 tb_shape) |
bbox and cql_filter both specified | 同时使用 bbox 参数和 CQL_FILTER | 将 bbox 条件合并到 CQL_FILTER 中 |
Could not parse CQL filter list | CQL 语法错误 | 检查函数名、括号、坐标顺序 |
InvalidParameterValue + Illegal property name | 字段名不存在 | 先调用 DescribeFeatureType 确认字段 |
Operation on mixed SRID geometries | SRID 不一致 | 使用 BBOX 而非 DWITHIN |
Function not found | 使用了不支持的函数 | 使用标准 CQL 函数,不加 ST_ 前缀 |
outputFormat not supported | 输出格式不支持 | 检查 GetCapabilities 中的 AcceptFormats |
| 返回 XML 错误但不是 JSON | 请求参数错误 | 检查 URL 参数是否正确编码 |
八、WFS vs WMS 选择指南
| 需求 | 推荐方案 | 说明 |
|---|---|---|
| 显示地图底图 | WMS | 返回图片,性能好 |
| 点击查询属性 | WMS GetFeatureInfo | 简单点查询,返回属性 |
| 框选查询 | WFS | 返回完整矢量数据,可高亮 |
| 获取几何数据进行分析 | WFS | 返回 GeoJSON 几何 |
| 数据下载/导出 | WFS | 返回矢量格式 |
| 查询后高亮显示 | WFS | 可加载为 GeoJsonDataSource |
| 编辑/更新数据 | WFS-T | 事务操作(需要配置) |
九、GeoServer 配置检查清单
| 检查项 | 操作 | 预期结果 |
|---|---|---|
| WFS 服务启用 | 服务 → WFS → 配置 → 勾选"启用服务" | ✅ 服务状态已启用 |
| 输出格式 | 检查 GetCapabilities 中的 outputFormat | ✅ 包含 json |
| 图层 WFS 发布 | 数据 → 图层 → 检查 WFS 服务已勾选 | ✅ WFS 复选框已勾选 |
| 几何字段类型 | 要素类型详细信息中查看几何字段 | ✅ 类型为 Geometry/Point/LineString/Polygon |
| 空间索引 | PostGIS 中检查 GIST 索引 | ✅ 存在 tb_shape 的索引 |
| CORS 配置 | web.xml 中配置 CORS 过滤器 | ✅ 允许跨域请求 |
十、最佳实践总结
-
版本选择:优先使用 WFS 1.1.0 + outputFormat=json
-
坐标顺序:牢记 WFS 1.1.0 的 BBOX 是 (纬度, 经度) 顺序
-
字段确认:必先用 DescribeFeatureType 确认几何字段名
-
CQL 一致性:WMS 和 WFS 使用完全相同的 CQL_FILTER
-
分步测试:先测试无过滤 → 属性过滤 → 空间过滤 → 混合过滤
-
错误处理:捕获 JSON 解析异常,检查是否为 XML 错误报告
-
性能优化:使用 BBOX 而非复杂几何函数,添加
maxFeatures限制 -
数据转换:WFS 返回的 GeoJSON 属性名是下划线格式(
task_id),需转换为驼峰(taskId)
WMS + WFS 框选查询完整知识点总结
一、核心概念
| 概念 | 说明 |
|---|---|
| WMS (Web Map Service) | 返回图片格式的地图数据,用于显示 |
| WFS (Web Feature Service) | 返回矢量格式的要素数据,用于查询 |
| GeoJSON | WFS 可返回的矢量数据格式,Cesium 可直接加载 |
| CQL (Contextual Query Language) | 扩展的查询语言,用于在 WMS/WFS 中过滤数据 |
| BBOX | 矩形空间查询函数,用于框选 |
二、架构设计
text
┌─────────────────────────────────────────────────────────────┐
│ 前端 Cesium 应用 │
├─────────────────────────────────────────────────────────────┤
│ WMS 图层显示 │ WFS 框选查询 │
│ ┌─────────────────┐ │ ┌─────────────────┐ │
│ │ addWMS() │ │ │ initBoxSelect() │ │
│ │ - 加载瓦片图层 │ │ │ - 绘制选框 │ │
│ │ - 应用 CQL 过滤 │ │ │ - 获取矩形范围 │ │
│ │ - 显示地图 │ │ │ - 调用 WFS 查询 │ │
│ └────────┬────────┘ │ └────────┬────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌─────────────────┐ │ ┌─────────────────┐ │
│ │ 相同 CQL 过滤 │◄─────┼──│ queryWFS...() │ │
│ └─────────────────┘ │ │ - BBOX 空间过滤 │ │
│ │ │ - 返回 GeoJSON │ │
│ │ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ GeoServer │
│ /jiesen/wms │
│ /jiesen/wfs │
└─────────────────┘
三、WMS 图层加载代码
public async addWMS(Bounds?: number[], CQL?: any) {
this.clearAllSelections();
this.removeWmsLayer();
const url = process.env.NODE_ENV === 'development'
? '/地址/wms'
: `${window.location.origin}/地址/wms`;
const layers = '图层名';
// 设置矩形范围
let rectangle: Cesium.Rectangle | undefined = undefined;
if (Bounds && Bounds.length === 4) {
rectangle = Cesium.Rectangle.fromDegrees(Bounds[0], Bounds[1], Bounds[2], Bounds[3]);
this.rectangle = rectangle;
}
// 构建 CQL 过滤条件
let parameters: any = {
format: 'image/png',
version: '1.1.1',
layers: layers,
transparent: true,
};
let finalCqlFilter: string;
parameters.CQL_FILTER = finalCqlFilter;
// ✅ 保存当前 CQL,供 WFS 查询使用
this.currentWmsCql = finalCqlFilter;
this.wmsImageryProvider = new Cesium.WebMapServiceImageryProvider({
url,
layers,
parameters,
crs: 'EPSG:4326', // WMS 1.3.0 使用 crs
srs: 'EPSG:4326', // WMS 1.1.1 使用 srs
tilingScheme: new Cesium.GeographicTilingScheme(),
rectangle: rectangle!,
minimumLevel: 0,
maximumLevel: 26,
enablePickFeatures: false // 关闭默认拾取,使用 WFS 查询
});
this.wmsLayer = this.viewer.imageryLayers.addImageryProvider(this.wmsImageryProvider);
this.wmsLayer.terrainClamp = true;
this.viewer.imageryLayers.raiseToTop(this.wmsLayer);
this.wmsLayer.samplingHeight = 100;
}
四、框选交互代码(带了一个框,可以自行去掉)
public initBoxSelect(callback?: (rect: { minLon: number; minLat: number; maxLon: number; maxLat: number; corners: any }) => void) {
this.handler != null && this.handler.destroy();
console.log('this.doubleHandler', this.doubleHandler);
this.offGon();
// 销毁旧的框选处理器
this.boxHandler && this.boxHandler.destroy();
this.boxHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
let isDrawing = false;
let startCarto: Cesium.Cartographic | null = null;
// 移除上次临时显示的矩形
if (this.activeShape) {
this.viewer.entities.remove(this.activeShape);
this.activeShape = null;
}
// 左键:第一次左键记录起点,第二次左键结束并回调
this.boxHandler.setInputAction(async (e: Cesium.ScreenSpaceEventHandler.PositionedEvent) => {
const position = e?.position;
if (!position) return;
const ray = this.viewer.camera.getPickRay(position);
if (!ray) return;
const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);
if (!cartesian) return;
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
const lon = Cesium.Math.toDegrees(cartographic.longitude);
const lat = Cesium.Math.toDegrees(cartographic.latitude);
if (!isDrawing) {
// 开始绘制
startCarto = cartographic;
isDrawing = true;
const coords = [lon, lat, lon, lat, lon, lat, lon, lat, lon, lat];
this.activeShape = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArray(coords)),
material: Cesium.Color.YELLOW.withAlpha(0.25),
outline: true,
outlineColor: Cesium.Color.YELLOW,
},
});
} else {
// 结束绘制
isDrawing = false;
const startLon = Cesium.Math.toDegrees(startCarto!.longitude);
const startLat = Cesium.Math.toDegrees(startCarto!.latitude);
const minLon = Math.min(startLon, lon);
const maxLon = Math.max(startLon, lon);
const minLat = Math.min(startLat, lat);
const maxLat = Math.max(startLat, lat);
const corners = [
{ lon: minLon, lat: minLat },
{ lon: maxLon, lat: minLat },
{ lon: maxLon, lat: maxLat },
{ lon: minLon, lat: maxLat },
];
// 回调结果
try {
if (this.activeShape) {
this.viewer.entities.remove(this.activeShape);
this.activeShape = null;
}
console.log('点到了', { minLon, minLat, maxLon, maxLat, corners });
// 在框选结束的回调位置,添加这行
const data = await this.queryWFSByRectangle(minLon, minLat, maxLon, maxLat);
callback && callback(data);
} catch (err) {
console.error('框选回调错误', err);
}
// 销毁事件处理器(如需保留图形,可注释掉下面两行)
this.boxHandler && this.boxHandler.destroy();
this.boxHandler = null;
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 鼠标移动时更新矩形显示
this.boxHandler.setInputAction((e: Cesium.ScreenSpaceEventHandler.MotionEvent) => {
if (!isDrawing || !startCarto) return;
const pos = e.endPosition;
const ray = this.viewer.camera.getPickRay(pos);
if (!ray) return;
const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);
if (!cartesian) return;
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
const lon = Cesium.Math.toDegrees(cartographic.longitude);
const lat = Cesium.Math.toDegrees(cartographic.latitude);
const startLon = Cesium.Math.toDegrees(startCarto.longitude);
const startLat = Cesium.Math.toDegrees(startCarto.latitude);
const minLon = Math.min(startLon, lon);
const maxLon = Math.max(startLon, lon);
const minLat = Math.min(startLat, lat);
const maxLat = Math.max(startLat, lat);
const coords = [minLon, minLat, maxLon, minLat, maxLon, maxLat, minLon, maxLat, minLon, minLat];
if (this.activeShape) {
(this.activeShape.polygon as any).hierarchy = new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArray(coords));
} else {
this.activeShape = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArray(coords)),
material: Cesium.Color.YELLOW.withAlpha(0.25),
outline: true,
outlineColor: Cesium.Color.YELLOW,
},
});
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
重点是
const data = await this.queryWFSByRectangle(minLon, minLat, maxLon, maxLat);
callback && callback(data);
五、WFS 框选查询代码
async queryWFSByRectangle(minLon: number, minLat: number, maxLon: number, maxLat: number) {
const wfsUrl = process.env.NODE_ENV === 'development'
? '/地址/wfs'
: `${window.location.origin}/地址/wfs`;
// ✅ 使用与 WMS 相同的 CQL 过滤条件
const cqlFilter = this.currentWmsCql;
// ✅ 关键:BBOX 的正确顺序是 (纬度, 经度)
const bboxCondition = `BBOX(tb_shape, ${minLat}, ${minLon}, ${maxLat}, ${maxLon})`;
const params = new URLSearchParams();
params.append('service', 'WFS');
params.append('version', '1.1.0');
params.append('request', 'GetFeature');
params.append('typeName', 'jiesen:tb_list');
params.append('outputFormat', 'json');
params.append('srsName', 'EPSG:4326');
params.append('CQL_FILTER', `${cqlFilter} AND ${bboxCondition}`);
const url = `${wfsUrl}?${params.toString()}`;
try {
const response = await fetch(url);
const geojson = await response.json();
console.log(`✅ 框选范围内找到 ${geojson.features?.length || 0} 个要素`);
return geojson;
} catch (error) {
console.error('WFS请求失败:', error);
return { features: [] };
}
}
七、调用方代码
实例类后
wayLineShowMap.initBoxSelect(async (rect: { minLon: number; minLat: number; maxLon: number; maxLat: number; corners: number[][] }) => {
// 调用 WFS 查询获取 GeoJSON
console.log('rect', rect);}}
八、关键注意事项
| 序号 | 注意事项 | 说明 |
|---|---|---|
| 1 | CQL 一致性 | WMS 和 WFS 必须使用完全相同的 CQL_FILTER,否则查询结果不一致 |
| 2 | BBOX 坐标顺序 | WFS 1.1.0 要求 BBOX(geom, minY, minX, maxY, maxX) 即 纬度在前,经度在后 |
| 3 | 几何字段名 | 几何字段名不是固定的 geom,需根据实际配置确定(如 tb_shape) |
| 4 | 事件清理 | 每次 initBoxSelect 前必须销毁旧的 boxHandler,回调后也要销毁 |
| 5 | 坐标系 | 使用 EPSG:4326,确保 WMS 和 WFS 的 srsName/crs 一致 |
| 6 | CORS 配置 | GeoServer 需要配置跨域,否则前端无法直接请求 |
| 7 | 防抖处理 | 框选回调可能重复触发,需要添加防抖或立即销毁事件 |
| 8 | 错误处理 | WFS 返回 XML 错误时,需要解析错误信息并妥善处理 |
| 9 | 输出格式 | WFS 请求需指定 outputFormat=json 以返回 GeoJSON |
| 10 | 版本兼容 | WMS 1.1.1 使用 srs,WMS 1.3.0 使用 crs |
| BBOX 坐标顺序 | WFS 1.1.0 要求 BBOX(geom, minY, minX, maxY, maxX) 即 纬度在前,经度在后 |
geom 的含义
在你的代码中,geom 是一个占位符,需要替换成你实际的几何字段名。
一、geom 是什么?
| 概念 | 说明 |
|---|---|
| geom | 数据库表中存储几何数据的列名(字段名) |
| 几何字段 | 存储点、线、面等空间数据的字段 |
| 不是固定名称 | 每个图层的几何字段名可能不同 |
二、如何找到你实际的几何字段名?
方法1:通过 GeoServer 界面查看
-
登录 GeoServer → 数据 → 图层
-
找到 你的图层名,点击图层名称
-
向下滚动到 "要素类型详细信息" 表格
-
找到类型为
Geometry的行,"属性"列的值就是几何字段名
九、常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Illegal property name: geom | 几何字段名错误 | 使用正确的字段名(如 tb_shape) |
bbox and cql_filter both specified | 同时使用 bbox 和 CQL 参数 | 将 bbox 条件合并到 CQL_FILTER 中 |
Could not parse CQL filter | CQL 语法错误 | 检查 BBOX 坐标顺序和函数名称 |
SRID mismatch | 几何 SRID 不一致 | 使用 BBOX 而非 DWITHIN |
URL is not valid | Demo requests 工具使用错误 | 直接用浏览器 URL 测试 |
| 回调执行两次 | 事件未正确销毁 | 回调后立即 destroy() |
十、数据流向图
text
用户点击地图
│
▼
initBoxSelect() 开启框选
│
├── 第一次点击 ──► 记录起点,显示黄色半透明矩形
│
├── 鼠标移动 ────► 实时更新矩形大小
│
└── 第二次点击 ──► 计算矩形范围
│
▼
callback({ minLon, minLat, maxLon, maxLat })
│
▼
queryWFSByRectangle()
│
├── 使用 currentWmsCql(与 WMS 一致)
├── 构建 BBOX 条件(纬度在前)
└── 发送 WFS 请求
│
▼
GeoServer
│
▼
GeoJSON 数据
│
▼
convertWfsToDataFormat()
│
▼
后端统一格式数据
│
▼
更新 selectedAreas,显示选中结果
274

被折叠的 条评论
为什么被折叠?



