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开发 相关内容,欢迎关注 !

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax