GeoTools 结合 OpenLayers 实现属性查询
前言
在GIS开发中,属性查询是非常普遍的操作,这是每一个GISer都要掌握的必备技能。实现高效的数据查询功能可以提升用户体验,完成数据的快速可视化表达。
本篇教程在之前一系列文章的基础上讲解如何将使用GeoTools
工具结合OpenLayers
实现PostGIS
空间数据库数据的属性查询功能。
开发环境
本文使用如下开发环境,以供参考。
:::block-1
时间:2025年
GeoTools:v34-SNAPSHOT
IDE:IDEA2025.1.2
JDK:v17
OpenLayers:v9.2.4
Layui:v2.9.14
:::
1. 搭建SpringBoot后端服务
本文接着OpenLayers 从后端服务加载 GeoJSON 数据进行讲解,如果还没有读过,请从那里开始。
:::block-1
在开始本文之前,请确保你已经安装好了PostgreSQL
数据库,添加了PostGIS
插件,并且已经启用空间数据拓展。安装完成之后,你还需要将Shapefile
导入空间数据库。如果你还不了解如何导入空间数据,可参考之前的文章。
:::
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. 创建Countries
实体
如下图是我测试导入的世界国家行政区数据。
可选取部分字段创建业务对象。
package com.example.geotoolsboot.dao;import lombok.Getter;
import lombok.Setter;public class Countries {@Setter@Getterpublic Integer gid; // 要素id@Setter@Getterpublic String sovereignt; // 国家名称@Setter@Getterpublic String sov_a3; // 国家名称缩写@Setter@Getterpublic String admin; // 国家名称@Setter@Getterpublic String adm0_a3; // 国家名称缩写@Setter@Getterpublic String geom; // 几何字段名称
}
1.3. 创建数据库连接
在项目中创建数据库连接工具类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"); // 明确指定schemapgParams.put(PostgisNGDataStoreFactory.EXPOSE_PK.key, true); // 暴露主键return pgParams;}
}
1.4. 创建属性查询实现类
在项目中创建PgService
类用于实现空间数据的属性过滤操作。定义一个方法attributeFilter
,该方法接收一个字符串参数,也就是属性过滤条件,如"admin = 'China'"
,最后将查询结果总数和查询数据返回。
使用CQL.toFilter
方法进行属性过滤。
// 读取 PostGIS 空间数据库数据,并实现属性过滤
public Map<String,Object> attributeFilter(String filterParams) throws Exception{// 存储返回对象Map<String,Object> result = new HashMap<>();// 国家数据列表List<Countries> countries = new ArrayList<>();// 创建数据库连接Map<String, Object> pgParams = PgUtils.connectPostGIS();DataStore dataStore = DataStoreFinder.getDataStore(pgParams);// 数据库表名String typeName = "countries";SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);// 创建数据过滤器Filter filter = CQL.toFilter(filterParams);// Filter filter = CQL.toFilter("admin = 'China'");SimpleFeatureCollection collection = featureSource.getFeatures(filter);// 统计查询总数int count = collection.size();result.put("count",count);try(FeatureIterator<SimpleFeature> features = collection.features()) {while (features.hasNext()) {SimpleFeature feature = features.next();// 构造返回数据Countries country = new Countries();country.setGid((Integer) feature.getAttribute("gid"));country.setAdmin((String) feature.getAttribute("admin"));country.setSovereignt((String) feature.getAttribute("sovereignt"));country.setSov_a3((String) feature.getAttribute("sov_a3"));country.setAdm0_a3((String) feature.getAttribute("adm0_a3"));Object geometry = feature.getAttribute("geom");GeometryJSON geometryJSON = new GeometryJSON();StringWriter writer = new StringWriter();geometryJSON.write((Geometry) geometry,writer);String geoJSON = writer.toString();country.setGeom(geoJSON);countries.add(country);}}catch (Exception e){e.printStackTrace();}result.put("countries",countries);return result;
}
1.5. 创建属性过滤控制层
在测试中,使用注解@CrossOrigin(origins = "*")
实现接口允许跨域,注解@GetMapping
添加请求访问路径。
package com.example.geotoolsboot.controller;
import com.example.geotoolsboot.service.impl.PgService;
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.util.Map;/*** 属性查询过滤器*/@CrossOrigin(origins = "*") // 允许跨域
@RestController
public class AttributeQueryController {@Autowiredprivate PgService pgService;@GetMapping("/countryList")public Map<String,Object> getCountriesByAttribute(@RequestParam(required = false) String filterParams) throws Exception{return pgService.attributeFilter(filterParams);}
}
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="aihao"><option value=""></option><option value="gid" selected>gid(要素编号)</option><option value="admin">admin(国家名称)</option><option value="adm0_a3">adm0_a3(国家名称缩写)</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="aihao"><option value=""></option><option value="<"><</option><option value=">" selected>></option><option value="=">=</option></select></div></div><div class="layui-form-item"><label class="layui-form-label">查询内容</label><div class="layui-input-block"><input type="text" name="content" lay-verify="required" placeholder="请输入查询内容" autocomplete="off"class="layui-input"></div></div><div class="layui-form-item"><label for="">一共查询到:</label><span class="resultCount">0</span><span>条数据</span></div><div class="layui-form-item"><button class="layui-btn" lay-submit lay-filter="attrQuery">确认</button><button type="reset" class="layui-btn layui-btn-primary">重置</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
清除原图层数据。使用const { field, condition, content } = data.field
解构查询表单数据。后面的代码内容都是之前写过的,也比较简单,就不令行讲解了。
layui.use(['form'], function () {const form = layui.form;const layer = layui.layer;// 提交事件form.on('submit(attrQuery)', function (data) {removeLayerByName("country", map)removeLayerByName("highlightLayer", map)// 获取表单字段值const { field, condition, content } = data.field// 查询参数const querfilterParams = `${field + " " + condition + " '" + content + "'"}`// 后端服务地址const JSON_URL = "http://127.0.0.1:8080/countryList?filterParams=" + querfilterParamsfetch(JSON_URL).then(response => response.json().then(result => {// 获取查询数量const resultCount = result.countdocument.querySelector(".resultCount").textContent = resultCount// 获取查询结果const countries = result.countriesconst features = countries.map(item => {const feat = {}feat.type = "Feature"feat.geometry = JSON.parse(item.geom)feat.properties = itemfeat.properties.color = `hsl(${Math.floor(Math.random() * 360)}, 100%, 50%)`const feature = new ol.format.GeoJSON().readFeature(feat)return feature})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().setCenter([108.76623301275802, 34.22939602197002])map.getView().setZoom(4.5)// 高亮图层const highlightLayer = new ol.layer.Vector({source: new ol.source.Vector({}),map: map,style: {"stroke-color": '#3CF9FF',"stroke-width": 2.5}})// Popup 模板const popupColums = [{name: "gid",comment: "要素编号"},{name: "admin",comment: "国家名称"},{name: "adm0_a3",comment: "简称"},{name: "color",comment: "颜色"}]// 高亮要素let highlightFeat = undefinedfunction 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) returnconst pixel = map.getEventPixel(evt.originalEvent)showPopupInfo(pixel)})// 监听地图鼠标点击事件map.on("click", evt => {console.log(evt.coordinate)// 若正在拖拽地图,则退出if (evt.dragging) returnshowPopupInfo(evt.pixel)})}))return false; // 阻止默认 form 跳转});});
OpenLayers示例数据下载,请回复关键字:ol数据
全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试
【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。
欢迎访问我的博客网站-长谈GIS:
http://shanhaitalk.com
都看到这了,不要忘记点赞、收藏 + 关注 哦 !
本号不定时更新有关 GIS开发 相关内容,欢迎关注 !