GeoTools 结合 OpenLayers 实现空间查询

前言

在GIS开发中,空间查询和属性查询一样,具有相当重要的地位,也是每一个GISer都要掌握的必备技能。实现高效的数据查询功能有利于提升用户体验,完成数据的快速可视化表达。

本篇教程在之前一系列文章的基础上讲解如何将使用GeoTools工具结合OpenLayers实现PostGIS空间数据库数据的空间查询功能。在正式开始本文之前,你需要了解以下GIS中常见的空间关系,可以参考文章:

  1. GIS 空间关系:九交模型
  2. 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文件中添加开发所需依赖,其中jdbcpostgresql依赖用于连接数据库。

xml 复制代码
<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参数中填写数据库连接信息。

typescript 复制代码
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工厂用于创建空间查询过滤器,例子中主要展示了intersectscontainsdisjoint三种空间关系。

ini 复制代码
/**
 * 读取 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添加请求访问路径。

在控制器中使用GeometryJSONGeoJSON字符串对象转换为Geometry几何对象。

ini 复制代码
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表单创建空间查询结构,包括空间查询条件以及绘制几何对象。

xml 复制代码
<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 结构样式:

css 复制代码
.query-wrap {
    position: absolute;
    padding: 10px;
    top: 80px;
    left: 90px;
    background: #ffffff;
    width: 250px;
    border-radius: 2.5px;
}

对于JS部分,在前端直接使用fetchAPI请求接口数据。在每次点击请求按钮后都需要调用工具类方法removeLayerByName清除原图层数据。

后面的代码内容都是之前写过的,也比较简单,就不另行讲解了。

less 复制代码
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对象。

php 复制代码
/**
 * 根据几何类型绘制几何对象
 */
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之路】 已经接入了智能助手,欢迎关注,欢迎提问。

欢迎访问我的博客网站-长谈GIShttp://shanhaitalk.com

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 !

相关推荐
你的人类朋友40 分钟前
🤔什么时候用BFF架构?
前端·javascript·后端
知识分享小能手1 小时前
Bootstrap 5学习教程,从入门到精通,Bootstrap 5 表单验证语法知识点及案例代码(34)
前端·javascript·学习·typescript·bootstrap·html·css3
一只小灿灿1 小时前
前端计算机视觉:使用 OpenCV.js 在浏览器中实现图像处理
前端·opencv·计算机视觉
前端小趴菜051 小时前
react状态管理库 - zustand
前端·react.js·前端框架
Jerry Lau2 小时前
go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
前端·golang·gin
我命由我123452 小时前
前端开发问题:SyntaxError: “undefined“ is not valid JSON
开发语言·前端·javascript·vue.js·json·ecmascript·js
0wioiw02 小时前
Flutter基础(前端教程③-跳转)
前端·flutter
落笔画忧愁e2 小时前
扣子Coze纯前端部署多Agents
前端
海天胜景2 小时前
vue3 当前页面方法暴露
前端·javascript·vue.js