GeoTools 结合 OpenLayers 实现属性查询

发布于:2025-06-29 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言

在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文件中添加开发所需依赖,其中jdbcpostgresql依赖用于连接数据库。

<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
    @Getter
    public Integer gid; // 要素id
    @Setter
    @Getter
    public String sovereignt; // 国家名称
    @Setter
    @Getter
    public String sov_a3; // 国家名称缩写
    @Setter
    @Getter
    public String admin; // 国家名称
    @Setter
    @Getter
    public String adm0_a3; // 国家名称缩写
    @Setter
    @Getter
    public 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"); // 明确指定schema
        pgParams.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 {

    @Autowired
    private 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="<">&lt</option>
                    <option value=">" selected>&gt</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部分,在前端直接使用fetchAPI请求接口数据。在每次点击请求按钮后都需要调用工具类方法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=" + querfilterParams
            fetch(JSON_URL).then(response => response.json()
                .then(result => {
                    // 获取查询数量
                    const resultCount = result.count
                    document.querySelector(".resultCount").textContent = resultCount
                    // 获取查询结果
                    const countries = result.countries
                    const features = countries.map(item => {
                        const feat = {}
                        feat.type = "Feature"
                        feat.geometry = JSON.parse(item.geom)
                        feat.properties = item
                        feat.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 = 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) return
                        const pixel = map.getEventPixel(evt.originalEvent)
                        showPopupInfo(pixel)
                    })

                    // 监听地图鼠标点击事件
                    map.on("click", evt => {
                        console.log(evt.coordinate)
                        // 若正在拖拽地图,则退出
                        if (evt.dragging) return
                        showPopupInfo(evt.pixel)
                    })
                })

            )
            return false; // 阻止默认 form 跳转
        });
    });

OpenLayers示例数据下载,请回复关键字:ol数据

全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试

【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。

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

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

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


网站公告

今日签到

点亮在社区的每一天
去签到