前言
❝在GIS开发中,空间查询和属性查询一样,具有相当重要的地位,也是每一个GISer都要掌握的必备技能。实现高效的数据查询功能有利于提升用户体验,完成数据的快速可视化表达。
本篇教程在之前一系列文章的基础上讲解如何将使用GeoTools
工具结合OpenLayers
实现PostGIS
空间数据库数据的空间查询功能。在正式开始本文之前,你需要了解以下GIS中常见的空间关系,可以参考文章:
-
GIS 空间关系:九交模型 -
GIS 空间关系:维度扩展九交模型
如果你还没有看过,建议从那里开始。
开发环境
本文使用如下开发环境,以供参考。
时间:2025年
GeoTools:v34-SNAPSHOT
IDE:IDEA2025.1.2
JDK:v17
OpenLayers:v9.2.4
Layui:v2.9.14
1. 搭建SpringBoot后端服务
在开始本文之前,请确保你已经安装好了PostgreSQL
数据库,添加了PostGIS
插件,并且已经启用空间数据拓展。安装完成之后,你还需要将Shapefile
导入空间数据库。如果你还不了解如何导入空间数据,可参考之前的文章。
将 Shp 导入 PostGIS 空间数据的五种方式(全)
1.1. 安装依赖
在pom.xml
文件中添加开发所需依赖,其中jdbc
和postgresql
依赖用于连接数据库。
<dependencies>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools.jdbc</groupId>
<artifactId>gt-jdbc-postgis</artifactId>
<version>${geotools.version}</version>
</dependency>
<!-- PostgreSQL 驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>osgeo</id>
<name>OSGeo Release Repository</name>
<url>https://repo.osgeo.org/repository/release/</url>
<snapshots><enabled>false</enabled></snapshots>
<releases><enabled>true</enabled></releases>
</repository>
<repository>
<id>osgeo-snapshot</id>
<name>OSGeo Snapshot Repository</name>
<url>https://repo.osgeo.org/repository/snapshot/</url>
<snapshots><enabled>true</enabled></snapshots>
<releases><enabled>false</enabled></releases>
</repository>
</repositories>
1.2. 创建数据库连接
在项目中创建数据库连接工具类PgUtils
,在Map
参数中填写数据库连接信息。
package com.example.geotoolsboot.utils;
import org.geotools.data.postgis.PostgisNGDataStoreFactory;
import java.util.HashMap;
import java.util.Map;
/**
* PostGIS 空间数据库工具类
*/
public class PgUtils {
public static Map<String, Object> connectPostGIS(){
// 连接PostGIS数据库
Map<String, Object> pgParams = new HashMap();
pgParams.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
pgParams.put(PostgisNGDataStoreFactory.HOST.key, "localhost");
pgParams.put(PostgisNGDataStoreFactory.PORT.key, "5432");
pgParams.put(PostgisNGDataStoreFactory.DATABASE.key, "geodata");
pgParams.put(PostgisNGDataStoreFactory.USER.key, "postgres");
pgParams.put(PostgisNGDataStoreFactory.PASSWD.key, "123456");
pgParams.put(PostgisNGDataStoreFactory.SCHEMA.key, "public"); // 明确指定schema
pgParams.put(PostgisNGDataStoreFactory.EXPOSE_PK.key, true); // 暴露主键
return pgParams;
}
}
1.3. 创建空间查询方法
在项目中创建PgService
类用于实现数据的空间过滤操作。定义一个方法spatialFilter
,该方法接收两个字符串参数,一个是空间关系类型,另一个是几何类型GeoJSON
字符串对象。
FilterFactory
工厂用于创建空间查询过滤器,例子中主要展示了intersects
、contains
、disjoint
三种空间关系。
/**
* 读取 PostGIS 空间数据库数据,并实现空间过滤
* @param queryType:空间过滤条件
* @param geometry:Geometry 类型 geoJSON字符串
* @return
* @throws Exception
*/
public Map<String,Object> spatialFilter(String queryType,Geometry geometry) throws Exception{
Map<String,Object> result = new HashMap<>();
Map<String, Object> pgParams = PgUtils.connectPostGIS();
DataStore dataStore = DataStoreFinder.getDataStore(pgParams);
// 数据库表名
String typeName = "countries";
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
// 创建数据过滤器
FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
// 获取几何属性字段名称
String geometryPropertyName = featureSource.getSchema().getGeometryDescriptor().getLocalName();
Filter filter = null;
switch (queryType.toLowerCase()) {
case "intersects":
filter = factory.intersects(factory.property(geometryPropertyName),factory.literal(geometry));
break;
case "contains":
filter = factory.contains(factory.property(geometryPropertyName),factory.literal(geometry));
break;
case "disjoint":
filter = factory.disjoint(factory.property(geometryPropertyName),factory.literal(geometry));
break;
}
try{
SimpleFeatureCollection collection = featureSource.getFeatures(filter);
int count = collection.size();
result.put("count",count);
FeatureJSON featureJSON = new FeatureJSON();
StringWriter writer = new StringWriter();
featureJSON.writeFeatureCollection(collection,writer);
String jsonFeatures = writer.toString();
result.put("countries",jsonFeatures);
}catch(Exception e){
e.printStackTrace();
}
return result;
}
1.4. 创建空间查询控制器
在测试中,使用注解@CrossOrigin(origins = "*")
实现接口允许跨域,注解@GetMapping
添加请求访问路径。
在控制器中使用GeometryJSON
将GeoJSON
字符串对象转换为Geometry
几何对象。
package com.example.geotoolsboot.controller;
import com.example.geotoolsboot.service.impl.PgService;
import org.geotools.geojson.geom.GeometryJSON;
import org.locationtech.jts.geom.Geometry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.StringReader;
import java.util.Map;
/**
* 空间查询过滤器
*/
@CrossOrigin(origins = "*") // 允许跨域
@RestController
public class SpatialQueryController {
@Autowired
private PgService pgService;
@GetMapping("/spatialQuery")
public Map<String,Object> getCountriesByGeometry(
@RequestParam(required = false) String queryType,String geoJSON) throws Exception{
GeometryJSON geometryJSON = new GeometryJSON(7);
StringReader reader = new StringReader(geoJSON);
// 测试数据
// String json = "{"type":"Polygon","coordinates":[[[86.4278949817791,30.501716387392523],[86.4278949817791,20.5173418567791],[97.220864,20.306404893220904],[105.5177384635582,26.8454668567791],[86.4278949817791,30.501716387392523]]]}";
Geometry geometry = geometryJSON.read(reader);
return pgService.spatialFilter(queryType,geometry);
}
}
2. 使用 OpenLayers 加载数据
具体使用情况请参考之前的文章:OpenLayers 加载GeoJSON的五种方式
本文前端使用OpenLayers
结合Layui
框架实现。主要借助Layui
表单创建空间查询结构,包括空间查询条件以及绘制几何对象。
<div class="query-wrap">
<form class="layui-form layui-form-pane" action="">
<div class="layui-form-item">
<label class="layui-form-label">空间关系</label>
<div class="layui-input-block">
<select name="field" lay-filter="query-select-filter">
<option value=""></option>
<option value="intersects" selected>intersects(相交)</option>
<option value="contains">contains(包含)</option>
<option value="disjoint">disjoint(相离)</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">绘制对象</label>
<div class="layui-input-block">
<select name="condition" lay-filter="draw-select-filter">
<option value="Point">点</option>
<option value="LineString" selected>线</option>
<option value="Polygon">面</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label for="">一共查询到:</label>
<span class="resultCount">0</span>
<span>条数据</span>
</div>
<div class="layui-form-item">
<button lay-submit lay-filter="clearAll" class="layui-btn layui-btn-primary">清除</button>
<button class="layui-btn" lay-submit lay-filter="spatialQuery">确认</button>
</div>
</form>
</div>
CSS
结构样式:
.query-wrap {
position: absolute;
padding: 10px;
top: 80px;
left: 90px;
background: #ffffff;
width: 250px;
border-radius: 2.5px;
}
对于JS部分,在前端直接使用fetch
API请求接口数据。在每次点击请求按钮后都需要调用工具类方法removeLayerByName
清除原图层数据。
后面的代码内容都是之前写过的,也比较简单,就不另行讲解了。
layui.use(['form'], function () {
const form = layui.form;
const layer = layui.layer;
// 默认空间过滤条件
let queryType = "intersects"
// 空间过滤类型
form.on('select(query-select-filter)', function (data) {
queryType = data.value; // 获得被选Shape(value)
});
// 绘制事件
form.on('select(draw-select-filter)', function (data) {
queryJSON = null
const value = data.value; // 获得被选中的值
drawShape(value)
});
// 清除事件
form.on("submit(clearAll)", function (data) {
// 清除所有数据
removeInteraction()
// 清除图形
vectorSource.clear()
removeLayerByName("country", map)
removeLayerByName("highlightLayer", map)
removeOverlayByName("overLay")
isHighlighted = false
return false; // 阻止默认 form 跳转
})
// 提交事件
form.on('submit(spatialQuery)', function (data) {
isHighlighted = true
const queryParam = encodeURIComponent(queryJSON)
if (highlightLayer) {
highlightLayer.getSource().clear()
}
// 后端服务地址
const JSON_URL = `http://127.0.0.1:8080/spatialQuery?queryType=${queryType}&geoJSON=${queryParam}`
fetch(JSON_URL).then(response => response.json()
.then(result => {
console.log("countries:", JSON.parse(result.countries))
removeLayerByName("country", map)
removeLayerByName("highlightLayer", map)
const resultCount = result.count
if (!resultCount) {
layer.msg("未查询到数据!")
return
}
document.querySelector(".resultCount").textContent = resultCount
const countries = JSON.parse(result.countries)
const feats = countries.features.forEach(feat => {
feat.properties.color = `hsl(${Math.floor(Math.random() * 360)}, 100%, 50%)`
})
const features = new ol.format.GeoJSON().readFeatures(countries)
const vectorSource = new ol.source.Vector({
features: features,
format: new ol.format.GeoJSON()
})
// 行政区矢量图层
const regionLayer = new ol.layer.Vector({
source: vectorSource,
style: {
"text-value": ["string", ['get', 'admin']],
'fill-color': ['string', ['get', 'color'], '#eee'],
}
})
regionLayer.set("layerName", "country")
map.addLayer(regionLayer)
map.getView().fit(features[0].getGeometry().getExtent())
map.getView().setZoom(4.5)
// 高亮图层
highlightLayer = new ol.layer.Vector({
source: new ol.source.Vector({}),
style: {
"stroke-color": '#3CF9FF',
"stroke-width": 2.5
}
})
highlightLayer.set("layerName", "highlightLayer")
map.addLayer(highlightLayer)
// Popup 模板
const popupColums = [
{
name: "gid",
comment: "要素编号"
},
{
name: "admin",
comment: "国家名称"
},
{
name: "adm0_a3",
comment: "简称"
},
{
name: "color",
comment: "颜色"
}
]
// 高亮要素
let highlightFeat = undefined
function showPopupInfo(pixel) {
regionLayer.getFeatures(pixel).then(features => {
// 若未查询到要素,则退出
if (!features.length) {
if (highlightFeat) {
highlightLayer.getSource().removeFeature(highlightFeat)
highlightFeat = undefined
}
return
}
// 获取要素属性
const properties = features[0].getProperties()
// 将事件坐标转换为地图坐标
const coords = map.getCoordinateFromPixel(pixel)
if (features[0] != highlightFeat) {
// 移除高亮要素
if (highlightFeat) {
highlightLayer.getSource().removeFeature(highlightFeat)
highlightFeat = undefined
}
highlightLayer.getSource().addFeature(features[0])
highlightFeat = features[0]
}
openPopupTable(properties, popupColums, coords)
})
}
// 监听地图鼠标移动事件
map.on("pointermove", evt => {
// 若正在拖拽地图,则退出
if (evt.dragging || !isHighlighted) return
const pixel = map.getEventPixel(evt.originalEvent)
showPopupInfo(pixel)
})
// 监听地图鼠标点击事件
map.on("click", evt => {
console.log(evt.coordinate)
// 若正在拖拽地图,则退出
if (evt.dragging || !isHighlighted) return
showPopupInfo(evt.pixel)
})
})
)
return false; // 阻止默认 form 跳转
});
});
创建drawShape
函数,用于绘制点、线和面等几何类型,removeInteraction
方法用于移除绘制控件。需要监听绘制完成事件,当绘制结束后,读取绘制要素并获取Geometry
对象。
/**
* 根据几何类型绘制几何对象
*/
function drawShape(type) {
let geometryFunction = null
drawInteraction = new ol.interaction.Draw({
source: vectorSource,
type,
geometryFunction,
style,
// freehand: true // 是否开启自由绘制模式
})
map.addInteraction(drawInteraction)
// 监听绘制事件
drawInteraction.on('drawend', evt => {
const feature = evt.feature
const featObj = new ol.format.GeoJSON().writeFeature(feature)
const geomtObj = new ol.format.GeoJSON().writeGeometry(feature.getGeometry())
queryJSON = geomtObj
})
}
// 移除绘制控件
function removeInteraction() {
if (drawInteraction) {
map.removeInteraction(drawInteraction)
}
}
❝OpenLayers示例数据下载,请回复关键字:ol数据
全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试
❝【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。
欢迎访问我的博客网站-长谈GIS:
http://shanhaitalk.com
都看到这了,不要忘记点赞、收藏 + 关注 哦 !
本号不定时更新有关 GIS开发 相关内容,欢迎关注 !