基于OpenLayers使用WFS实现GeoServer地图要素的增删改查
1. 概述
Web Feature Service(WFS)接口标准定义了一组接口,用于在Internet上访问要素和要素属性级别的地理信息。WFS提供了检索或查询矢量要素的方法,这种方法独立于它们发布的底层数据存储,WFS还可以更新或删除矢量要素。WFS的实例还能够存储查询,以便使客户机应用程序能够在稍后的时间点检索或执行查询
更多信息可参考:
- WFS - Introduction — OGC e-Learning 2.0.0 documentation (opengeospatial.github.io)
- Web Feature Service | OGC
中文版站点(翻译不完全准确):WFS-简介 — OGC e-Learning 2.0.0 文档 (osgeo.cn)
WFS服务器必须支持的操作:
-
GetCapabilities:返回一个文档,该文档描述由服务器提供的WFS服务提供的功能和资源
-
DescribeFeatureType:返回WFS实例提供或接受的功能类型和功能属性的结构描述
-
ListStoredQueries:返回存储在WFS实例中的查询列表
-
DescribeStoredQueries:返回存储在WFS实例中的查询的说明
-
GetFeature:从通过WFS发布的数据存储中返回要素实例的选择
WFS服务器通常也支持的操作:
- GetPropertyValue:检索一组要素实例的要素特性值或复杂要素特性值的一部分
- GetFeatureWithLock:提供与GetFeature请求类似的功能,但具有锁定特性的附加功能,可能是为了后续更新或更改
- LockFeature:锁定一组要素实例,以便在锁定到位时,其他操作都不能修改数据
- Transaction:允许插入、更新或删除要素实例及其属性
- CreateStoredQuery:创建并存储一个查询,客户机可以在稍后的时间点快速地触发该查询
- DropStoredQuery:从服务器中删除以前存储的查询
参考:WFS参考 — GeoServer 2.19.x User Manual (osgeo.cn)
GeoServer作为GIS服务器,支持WFS定义的一系列操作
WFS操作大部分支持KVP格式(URL里包含键值对的形式)和XML格式,少部分只支持XML格式(主要是更新与新增操作)
编写XML终究繁琐,OpenLayers支持OGC的WFS规范,可以基于WFS来对空间数据进行增、删、改、查(CRUD)
本文基于OpenLayers提供的WFS操作API,实现对GeoServer发布的空间数据进行增、删、改、查
本文大量参考了这篇文章:OpenLayers教程:网络要素服务(WFS) - 知乎 (zhihu.com)
2. 环境准备
GeoServer的下载与配置跨域可参考:
从官方下载的GeoServer带有示例数据,参考:
登录GeoServer,默认登录账户密码是:
- 用户名:
admin
- 密码:
geoserver
笔者这里使用的数据是 Tasmania roads:
另外,需要将此数据开启读写权限:
使用的OpenLayers版本是最新版6.15.1,CDN引入:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css"> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script>
3. 查询要素
这里的查询要素主要是指使用GetFeature
操作,即获取要素
GetFeature
操作可以使用KVP格式构造URL地址,参考官方自带示例:
可以通过构造URL来实现多种查询,构造的具体参数可以参考:
笔者这里查询Tasmania roads的所有要素并返回JSON格式:
http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json
此外我们还可以使用GeoServer自带的图层预览来复制构造的URL:
直接在new ol.source.Vector
中写入url参数,就可以利用WFS的KVP进行要素的查询,核心代码如下:
const vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json', format: new ol.format.GeoJSON() }) }); map.addLayer(vectorLayer);
完整代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Test</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css"> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script> <style> html, body { height: 100%; } body { margin: 0; padding: 0; } #map { height: 100%; } </style> </head> <body> <div id="map"></div> <script> const map = new ol.Map({ target: 'map', layers: [], view: new ol.View({ center: [147.01, -42.26], zoom: 8, projection: 'EPSG:4326' }) }); map.addLayer(new ol.layer.WebGLTile({ source: new ol.source.OSM() })); const vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json', format: new ol.format.GeoJSON() }), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#0000ff', width: 2 }) }) }); map.addLayer(vectorLayer); </script> </body> </html>
加载的地图如下:
4. 添加要素
更新、添加、删除要素操作都不支持KVP格式,所以只能使用XML文档的形式来请求
一个完整的XML字段解释如下图:
参考GeoServer自带的添加要素的示例:
编写XML字符过于繁琐,OpenLayers提供了编写WFS XML的API:
参考OpenLayers的API文档:OpenLayers v6.15.1 API - Class: WFS
核心代码如下:
// 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } function addFeatureWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( features, null, null, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ); // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { console.log(res); }); }
完整代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Test</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css"> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script> <style> html, body { height: 100%; } body { margin: 0; padding: 0; } #map { height: 100%; } </style> </head> <body> <button id="draw">绘制</button> <button id="add">添加</button> <button id="exit">退出</button> <div id="map"></div> <script> const map = new ol.Map({ target: 'map', layers: [], view: new ol.View({ center: [147.01, -42.26], zoom: 8, projection: 'EPSG:4326' }) }); map.addLayer(new ol.layer.WebGLTile({ source: new ol.source.OSM() })); const vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json', format: new ol.format.GeoJSON() }), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#0000ff', width: 2 }) }) }); map.addLayer(vectorLayer); // 绘制控件 let drawedFeatures = []; let draw = new ol.interaction.Draw({ source: vectorLayer.getSource(), type: 'MultiLineString', geometryName: 'the_geom' // 注意:这里图形信息字段一定要和后端服务器一致 }); draw.on('drawend', (event) => { let feature = event.feature; // 为要素添加属性,我这里随便设置属性 feature.set('TYPE', 'highway'); drawedFeatures.push(event.feature); }); map.addInteraction(draw); draw.setActive(false); document.querySelector('#add').addEventListener('click', (event) => { // 将绘制的要素添加到后台 addFeatureWFS(drawedFeatures); }); document.querySelector('#draw').addEventListener('click', (event) => { // 绘制要添加的要素 draw.setActive(true); }); document.querySelector('#exit').addEventListener('click', (event) => { // 退出绘制 draw.setActive(false); }); // 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } function addFeatureWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( features, null, null, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ); // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { let transactRes = WFS.readTransactionResponse(res); let str = transactRes.transactionSummary.totalInserted + " totalInserted!, insertIds: " + transactRes.insertIds + "\n"; str += transactRes.transactionSummary.totalUpdated + " totalUpdated!\n"; str += transactRes.transactionSummary.totalDeleted + " totalDeleted!"; alert(str); }); } </script> </body> </html>
注意:
geometryName
要和后端GIS服务器的数据保持一致,它将会被写为XML标签,可以从GetFeature
结果查询,参考下图WFS.writeTransaction
中的featureType
参数是图层名字,它将会被写为XML标签,参考下图
增加要素后的结果:
5. 更新要素
使用OpenLayers,更新要素的流程:
- 加载初始要素
- 创建选择控件选择要素
- 创建修改控件修改要素
- 将修改后的要素和信息写为XML
- 序列化XML
- 提交XML到服务器
核心代码如下:
// 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } // 选择要素控件 let select = new ol.interaction.Select({ layers: [vectorLayer], hitTolerance: 10 }); map.addInteraction(select); // 修改要素控件 let modify = new ol.interaction.Modify({ features: select.getFeatures() }); // 保存修改完成的要素 let modifiedFeatures = null; // 用于保存被修改的要素 modify.on('modifyend', (event) => { modifiedFeatures = event.features; }); map.addInteraction(modify); document.querySelector('#modify').addEventListener('click', (event) => { let features = []; modifiedFeatures.forEach(function (item, index, array) { features.push(item); }); updateWFS(features); }); function updateWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( null, features, null, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ) // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { console.log(res); }); }
完整代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Test</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css"> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script> <style> html, body { height: 100%; } body { margin: 0; padding: 0; } #map { height: 100%; } </style> </head> <body> <button id="select">选择</button> <button id="modify">提交</button> <button id="exit">退出</button> <div id="map"></div> <script> const map = new ol.Map({ target: 'map', layers: [], view: new ol.View({ center: [147.01, -42.26], zoom: 8, projection: 'EPSG:4326' }) }); map.addLayer(new ol.layer.WebGLTile({ source: new ol.source.OSM() })); const vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json', format: new ol.format.GeoJSON({ geometryName: 'the_geom' }) }), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#0000ff', width: 2 }) }) }); map.addLayer(vectorLayer); document.querySelector('#select').addEventListener('click', (event) => { select.setActive(true); }); document.querySelector('#exit').addEventListener('click', (event) => { select.setActive(false); }); // 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } // 选择要素控件 let select = new ol.interaction.Select({ layers: [vectorLayer], hitTolerance: 10 }); map.addInteraction(select); select.setActive(false); // 修改要素控件 let modify = new ol.interaction.Modify({ features: select.getFeatures() }); // 保存修改完成的要素 let modifiedFeatures = null; // 用于保存被修改的要素 modify.on('modifyend', (event) => { modifiedFeatures = event.features; }); map.addInteraction(modify); document.querySelector('#modify').addEventListener('click', (event) => { let features = []; modifiedFeatures.forEach(function (item, index, array) { features.push(item); }); updateWFS(features); }); function updateWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( null, features, null, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ) // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { let transactRes = WFS.readTransactionResponse(res); let str = transactRes.transactionSummary.totalInserted + " totalInserted!, insertIds: " + transactRes.insertIds + "\n"; str += transactRes.transactionSummary.totalUpdated + " totalUpdated!\n"; str += transactRes.transactionSummary.totalDeleted + " totalDeleted!"; alert(str); }); } </script> </body> </html>
注意:
- 此处加载WFS时需要指定
geometryName
- OpenLayers控件的使用可以参考官方API,Modify控件在Select控件选择后跟随圆点拖拽就行
修改后的结果如下:
6. 删除要素
使用OpenLayers,删除要素的流程:
- 加载初始要素
- 创建选择控件选择要素
- 将选择的要素和信息写为XML
- 序列化XML
- 提交XML到服务器
核心代码如下:
// 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } // 选择要素控件 let select = new ol.interaction.Select({ layers: [vectorLayer], hitTolerance: 10 }); map.addInteraction(select); document.querySelector('#delete').addEventListener('click', (event) => { let features = []; select.getFeatures().forEach(function (item, index, array) { features.push(item); }); deleteWFS(features); }) function deleteWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( null, null, features, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ) // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { console.log(res); }); }
完整代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Test</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/css/ol.css" type="text/css"> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.15.1/build/ol.js"></script> <style> html, body { height: 100%; } body { margin: 0; padding: 0; } #map { height: 100%; } </style> </head> <body> <button id="select">选择</button> <button id="delete">删除</button> <button id="exit">退出</button> <div id="map"></div> <script> const map = new ol.Map({ target: 'map', layers: [], view: new ol.View({ center: [147.01, -42.26], zoom: 8, projection: 'EPSG:4326' }) }); map.addLayer(new ol.layer.WebGLTile({ source: new ol.source.OSM() })); const vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'http://localhost:8080/geoserver/topp/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=topp:tasmania_roads&outputFormat=application/json', format: new ol.format.GeoJSON({ geometryName: 'the_geom' }) }), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#0000ff', width: 2 }) }) }); map.addLayer(vectorLayer); document.querySelector('#select').addEventListener('click', (event) => { select.setActive(true); }); document.querySelector('#exit').addEventListener('click', (event) => { select.setActive(false); }); // 服务配置,命名空间、图层、服务地址等 const geoserverData = { workSpaceName: 'topp', uri: 'http://www.openplans.org/topp', wfsURL: 'http://localhost:8080/geoserver/topp/ows?', layer: 'tasmania_roads' } // 选择要素控件 let select = new ol.interaction.Select({ layers: [vectorLayer], hitTolerance: 10 }); map.addInteraction(select); select.setActive(false); document.querySelector('#delete').addEventListener('click', (event) => { let features = []; select.getFeatures().forEach(function (item, index, array) { features.push(item); }); deleteWFS(features); }) function deleteWFS(features) { let WFS = new ol.format.WFS(); // 生成XML格式的WFS请求信息 let transact_xml = WFS.writeTransaction( null, null, features, { srcName: 'EPSG:4326', featureNS: geoserverData.uri, featurePrefix: geoserverData.workSpaceName, featureType: [geoserverData.layer], } ) // 将XML格式请求信息序列化为字符串格式 transact_str = (new XMLSerializer()).serializeToString(transact_xml); // 使用Fetch将请求发送到后端 fetch('http://localhost:8080/geoserver/wfs', { method: 'POST', body: transact_str, headers: { 'Content-Type': 'text/xml' } }).then(res => res.text()).then(res => { let transactRes = WFS.readTransactionResponse(res); let str = transactRes.transactionSummary.totalInserted + " totalInserted!, insertIds: " + transactRes.insertIds + "\n"; str += transactRes.transactionSummary.totalUpdated + " totalUpdated!\n"; str += transactRes.transactionSummary.totalDeleted + " totalDeleted!"; alert(str); }); } </script> </body> </html>
删除后的结果如下:
7. 参考资料
[1]WFS - Introduction — OGC e-Learning 2.0.0 documentation (opengeospatial.github.io)
[2]WFS-简介 — OGC e-Learning 2.0.0 文档 (osgeo.cn)
[3]WFS参考 — GeoServer 2.19.x User Manual (osgeo.cn)
[4]使用.net c# 对GeoServer要素进行Insert和Delete操作_我是小怪兽-的博客-CSDN博客_c# geoserver
[5]开源GIS(十五)——openlayers通过geoserver中WFS删除要素_gis_morningsun的博客-CSDN博客
[6]OGC的网络要素服务(WFS)(持续更新。。。) - 知乎 (zhihu.com)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了