2 geotools入门示例

1. 设置 Spring Boot 项目并集成 GeoTools 依赖

首先,你需要创建一个新的 Spring Boot 项目。你可以使用 Spring Initializr 来快速生成项目骨架。

选择以下依赖:

  • Web: Spring Web (用于创建 REST API)
  • Developer Tools: Spring Boot DevTools (可选,用于热加载)

添加 GeoTools 依赖:

在你的 pom.xml (如果你使用 Maven) 或 build.gradle (如果你使用 Gradle) 文件中添加 GeoTools 的依赖。GeoTools 是一个庞大的库,你可以只添加你需要的模块。为了读取 Shapefile 和 GeoJSON,你至少需要以下依赖:

Maven (pom.xml):

xml 复制代码
<properties>
    <java.version>17</java.version>
    <geotools.version>29.1</geotools.version>  </properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-main</artifactId>
        <version>${geotools.version}</version>
    </dependency>

    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-shapefile</artifactId>
        <version>${geotools.version}</version>
    </dependency>

    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-data</artifactId> <version>${geotools.version}</version>
    </dependency>
    <dependency>
        <groupId>org.geotools.xsd</groupId>
        <artifactId>gt-xsd-geojson</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>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope> </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </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>

注意: 请将 ${geotools.version} 替换为最新的 GeoTools 稳定版本。你可以在 GeoTools 官网 或 Maven 中央仓库查找最新版本。

2. 读取常见的空间数据格式

理解核心 GeoTools 概念

在开始读取数据之前,我们先来理解几个 GeoTools 的核心概念:

  • DataStore : DataStore 是访问特定数据格式或服务的入口点。例如,ShapefileDataStore 用于 Shapefile,GeoJSONDataStore 用于 GeoJSON 文件,JDBCDataStore 用于数据库。
  • DataStoreFactorySpi : 这是用于创建 DataStore 实例的工厂接口。例如,ShapefileDataStoreFactoryGeoJSONDataStoreFactoryJDBCDataStoreFactory (具体到 PostGIS 是 PostGISDialectFactoryJDBCDataStoreFactory 结合使用)。
  • FeatureSource : 一旦你有了 DataStore,你可以通过它获取 FeatureSourceFeatureSource 代表了一层地理要素 (features),你可以从中读取要素。它通常是只读的,如果需要写入,则使用其子接口 FeatureStore
  • FeatureCollection : FeatureCollection 是从 FeatureSource 中检索到的要素的集合。
  • FeatureIterator : FeatureIterator 用于遍历 FeatureCollection 中的每一个要素。重要的是:使用完毕后一定要关闭 FeatureIterator 以释放资源。
  • SimpleFeature : SimpleFeature 代表一个单独的地理要素,它包含了地理属性 (geometry) 和非地理属性 (attributes)。它的结构由 SimpleFeatureType 定义。
读取 Shapefile

假设你有一个名为 your_shapefile.shp 的 Shapefile 文件 (通常还伴随着 .dbf, .shx 等辅助文件)。

创建一个服务类来处理数据读取,例如 SpatialDataService.java

java 复制代码
package com.example.geotoolsdemo.service;

import org.geotools.api.data.*;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SpatialDataService {

    public List<Map<String, Object>> readShapefile(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new IOException("Shapefile not found at: " + filePath);
        }

        Map<String, Object> params = new HashMap<>();
        try {
            params.put("url", file.toURI().toURL());
        } catch (MalformedURLException e) {
            throw new RuntimeException("Failed to convert file path to URL", e);
        }
        params.put("create spatial index", true); // 可选,提高性能

        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
        ShapefileDataStore dataStore = null;
        List<Map<String, Object>> featuresList = new ArrayList<>();

        try {
            dataStore = (ShapefileDataStore) dataStoreFactory.createDataStore(params);
            if (dataStore == null) {
                throw new IOException("Could not create ShapefileDataStore for: " + filePath);
            }

            String typeName = dataStore.getTypeNames()[0]; // Shapefile 通常只包含一个类型
            FeatureSource<SimpleFeatureType, SimpleFeature> source = dataStore.getFeatureSource(typeName);
            FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures();

            try (FeatureIterator<SimpleFeature> features = collection.features()) {
                while (features.hasNext()) {
                    SimpleFeature feature = features.next();
                    Map<String, Object> featureAttributes = new HashMap<>();
                    feature.getProperties().forEach(property -> {
                        // GeoTools 的 geometry 对象不能直接序列化为 JSON,
                        // 在 REST API 中通常会转换为 GeoJSON 格式的字符串或 WKT
                        if (property.getValue() instanceof com.locationtech.jts.geom.Geometry) {
                            // 在 API 控制器中处理几何对象的 GeoJSON 转换
                            featureAttributes.put(property.getName().getLocalPart(), property.getValue().toString()); // 暂时用 WKT
                        } else {
                            featureAttributes.put(property.getName().getLocalPart(), property.getValue());
                        }
                    });
                    featuresList.add(featureAttributes);
                }
            }
        } finally {
            if (dataStore != null) {
                dataStore.dispose(); // 非常重要:释放资源
            }
        }
        return featuresList;
    }
}

重要:

  • 确保你的 Shapefile 路径正确。
  • 使用 try-with-resources 或者在 finally 块中调用 dataStore.dispose()features.close() 来释放资源,这非常重要,否则可能导致文件锁等问题。
读取 GeoJSON

GeoTools 提供了两种主要方式来处理 GeoJSON:

  1. 使用 GeoJSONDataStoreFactory : 这种方式与其他 DataStore 类似,更通用。
  2. 直接使用 gt-geojson 模块 (例如 GeoJSONReaderFeatureJSON) : 这种方式更直接,有时更简单,特别是当你只需要读取 GeoJSON 内容而不一定需要完整的 DataStore 抽象时。

方法 1: 使用 GeoJSONDataStoreFactory

java 复制代码
package com.example.geotoolsdemo.service;

// ... 其他 import ...
import org.geotools.data.geojson.GeoJSONDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;

// ... 在 SpatialDataService.java 中添加以下方法 ...

public List<Map<String, Object>> readGeoJSON(String filePath) throws IOException {
    File file = new File(filePath);
    if (!file.exists()) {
        throw new IOException("GeoJSON file not found at: " + filePath);
    }

    Map<String, Object> params = new HashMap<>();
    try {
        params.put(GeoJSONDataStoreFactory.URLP.key, file.toURI().toURL());
    } catch (MalformedURLException e) {
        throw new RuntimeException("Failed to convert file path to URL", e);
    }

    GeoJSONDataStoreFactory dataStoreFactory = new GeoJSONDataStoreFactory();
    DataStore dataStore = null;
    List<Map<String, Object>> featuresList = new ArrayList<>();

    try {
        dataStore = dataStoreFactory.createDataStore(params);
        if (dataStore == null) {
            throw new IOException("Could not create GeoJSONDataStore for: " + filePath);
        }

        String typeName = dataStore.getTypeNames()[0];
        SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
        SimpleFeatureCollection collection = featureSource.getFeatures();

        try (FeatureIterator<SimpleFeature> features = collection.features()) {
            while (features.hasNext()) {
                SimpleFeature feature = features.next();
                Map<String, Object> featureAttributes = new HashMap<>();
                feature.getProperties().forEach(property -> {
                    if (property.getValue() instanceof com.locationtech.jts.geom.Geometry) {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue().toString()); // 暂时用 WKT
                    } else {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue());
                    }
                });
                featuresList.add(featureAttributes);
            }
        }
    } finally {
        if (dataStore != null) {
            dataStore.dispose();
        }
    }
    return featuresList;
}

方法 2: 直接使用 gt-geojson (例如 FeatureJSON)

这个模块允许你更直接地将 GeoJSON 字符串或流解析为 FeatureCollection

java 复制代码
package com.example.geotoolsdemo.service;

// ... 其他 import ...
import org.geotools.geojson.feature.FeatureJSON; // 用于解析和编码 FeatureCollection
import org.geotools.feature.FeatureCollection;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;

import java.io.FileInputStream;
import java.io.InputStream;

// ... 在 SpatialDataService.java 中添加以下方法 ...

public List<Map<String, Object>> readGeoJSONDirectly(String filePath) throws IOException {
    File file = new File(filePath);
    if (!file.exists()) {
        throw new IOException("GeoJSON file not found at: " + filePath);
    }

    List<Map<String, Object>> featuresList = new ArrayList<>();
    FeatureJSON fjson = new FeatureJSON(); // 用于读取 FeatureCollection

    try (InputStream in = new FileInputStream(file)) {
        FeatureCollection<SimpleFeatureType, SimpleFeature> collection = fjson.readFeatureCollection(in);

        try (FeatureIterator<SimpleFeature> features = collection.features()) {
            while (features.hasNext()) {
                SimpleFeature feature = features.next();
                Map<String, Object> featureAttributes = new HashMap<>();
                feature.getProperties().forEach(property -> {
                    if (property.getValue() instanceof com.locationtech.jts.geom.Geometry) {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue().toString()); // 暂时用 WKT
                    } else {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue());
                    }
                });
                featuresList.add(featureAttributes);
            }
        }
    }
    return featuresList;
}

选择哪种方式取决于你的具体需求和偏好。DataStore 方式更通用,而直接解析更轻量级。

PostGIS/其他空间数据库 (初步了解)

使用 JDBCDataStoreFactory 可以连接到多种支持 JDBC 的空间数据库,包括 PostGIS。

你需要:

  1. PostGIS (或其他空间数据库) 的 JDBC驱动 (例如 postgresql 驱动)。
  2. 数据库连接参数 (主机, 端口, 数据库名, 用户名, 密码等)。
java 复制代码
package com.example.geotoolsdemo.service;

// ... 其他 import ...
import org.geotools.data.DataStoreFinder;
import org.geotools.data.postgis.PostgisNGDataStoreFactory; // 推荐使用NG (Next Generation) 版本

// ... 在 SpatialDataService.java 中添加以下方法 ...

public List<Map<String, Object>> readPostGIS(String tableName) throws IOException {
    Map<String, Object> params = new HashMap<>();
    params.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
    params.put(PostgisNGDataStoreFactory.HOST.key, "localhost"); // 你的数据库主机
    params.put(PostgisNGDataStoreFactory.PORT.key, 5432);        // 你的数据库端口
    params.put(PostgisNGDataStoreFactory.DATABASE.key, "your_database"); // 你的数据库名
    params.put(PostgisNGDataStoreFactory.SCHEMA.key, "public");    // 你的模式名 (通常是 public)
    params.put(PostgisNGDataStoreFactory.USER.key, "your_user");      // 你的用户名
    params.put(PostgisNGDataStoreFactory.PASSWD.key, "your_password");  // 你的密码
    // params.put(PostgisNGDataStoreFactory.SSL_MODE.key, "disable"); // 根据你的 SSL 配置

    DataStore dataStore = null;
    List<Map<String, Object>> featuresList = new ArrayList<>();

    try {
        // 使用 DataStoreFinder 自动查找合适的工厂
        dataStore = DataStoreFinder.getDataStore(params);

        if (dataStore == null) {
            throw new IOException("Could not connect to PostGIS database. Check connection parameters.");
        }

        // 或者直接使用 PostgisNGDataStoreFactory
        // PostgisNGDataStoreFactory factory = new PostgisNGDataStoreFactory();
        // if (!factory.canProcess(params)) {
        //     throw new IOException("PostgisNGDataStoreFactory cannot process the provided parameters.");
        // }
        // dataStore = factory.createDataStore(params);


        SimpleFeatureSource featureSource = dataStore.getFeatureSource(tableName); // tableName 是数据库中的表名
        SimpleFeatureCollection collection = featureSource.getFeatures();

        try (FeatureIterator<SimpleFeature> features = collection.features()) {
            while (features.hasNext()) {
                SimpleFeature feature = features.next();
                Map<String, Object> featureAttributes = new HashMap<>();
                feature.getProperties().forEach(property -> {
                    if (property.getValue() instanceof com.locationtech.jts.geom.Geometry) {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue().toString()); // 暂时用 WKT
                    } else {
                        featureAttributes.put(property.getName().getLocalPart(), property.getValue());
                    }
                });
                featuresList.add(featureAttributes);
            }
        }
    } finally {
        if (dataStore != null) {
            dataStore.dispose();
        }
    }
    return featuresList;
}

注意:

  • 确保已将 PostgreSQL JDBC 驱动添加到项目的依赖中。
  • 替换上述代码中的数据库连接参数为你自己的配置。
  • DataStoreFinder.getDataStore(params) 会尝试根据参数找到合适的 DataStoreFactory。对于 PostGIS,通常会找到 PostgisNGDataStoreFactory

3. 创建一个简单的 Spring Boot REST API 返回空间数据 (GeoJSON 格式)

现在我们来创建一个 REST 控制器,它将使用 SpatialDataService 读取数据,并将数据转换为 GeoJSON 格式返回。

GeoTools 的 gt-geojson 模块中的 FeatureJSON 类可以非常方便地将 FeatureCollection 或单个 Feature 编码为 GeoJSON 字符串。

创建 SpatialDataController.java:

Java 复制代码
package com.example.geotoolsdemo.controller;

import com.example.geotoolsdemo.service.SpatialDataService;
import org.geotools.api.data.*;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.geojson.feature.FeatureJSON; // 用于编码为 GeoJSON
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/spatial")
public class SpatialDataController {

    @Autowired
    private SpatialDataService spatialDataService;

    // 将 FeatureCollection 转换为 GeoJSON 字符串的辅助方法
    private String convertFeatureCollectionToGeoJSON(FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection) throws IOException {
        StringWriter writer = new StringWriter();
        FeatureJSON featureJSON = new FeatureJSON();
        featureJSON.writeFeatureCollection(featureCollection, writer);
        return writer.toString();
    }

    @GetMapping(value = "/shapefile", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getShapefileAsGeoJSON(@RequestParam String filePath) {
        try {
            File file = new File(filePath);
            Map<String, Object> params = new HashMap<>();
            params.put("url", file.toURI().toURL());
            params.put("create spatial index", false);

            ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
            ShapefileDataStore dataStore = (ShapefileDataStore) dataStoreFactory.createDataStore(params);
            if (dataStore == null) {
                 return ResponseEntity.status(500).body("{\"error\":\"Could not create ShapefileDataStore for: " + filePath + "\"}");
            }

            String typeName = dataStore.getTypeNames()[0];
            FeatureSource<SimpleFeatureType, SimpleFeature> source = dataStore.getFeatureSource(typeName);
            FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures();

            String geoJson = convertFeatureCollectionToGeoJSON(collection);

            dataStore.dispose(); // 确保释放资源
            return ResponseEntity.ok(geoJson);

        } catch (MalformedURLException e) {
            return ResponseEntity.status(500).body("{\"error\":\"Invalid file path URL: " + e.getMessage() + "\"}");
        } catch (IOException e) {
            return ResponseEntity.status(500).body("{\"error\":\"Error reading Shapefile: " + e.getMessage() + "\"}");
        }
    }


    @GetMapping(value = "/geojson-file", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getGeoJSONFileAsGeoJSON(@RequestParam String filePath) {
        // 对于 GeoJSON 文件,我们实际上可以直接返回其内容,
        // 但为了演示 GeoTools 的处理流程,我们先读取再写回。
        try {
            File file = new File(filePath);
             Map<String, Object> params = new HashMap<>();
            params.put(org.geotools.data.geojson.GeoJSONDataStoreFactory.URLP.key, file.toURI().toURL());

            org.geotools.data.geojson.GeoJSONDataStoreFactory dataStoreFactory = new org.geotools.data.geojson.GeoJSONDataStoreFactory();
            DataStore dataStore = dataStoreFactory.createDataStore(params);

            if (dataStore == null) {
                return ResponseEntity.status(500).body("{\"error\":\"Could not create GeoJSONDataStore for: " + filePath + "\"}");
            }

            String typeName = dataStore.getTypeNames()[0];
            SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
            SimpleFeatureCollection collection = featureSource.getFeatures();

            String geoJson = convertFeatureCollectionToGeoJSON(collection);
            dataStore.dispose();
            return ResponseEntity.ok(geoJson);

        } catch (MalformedURLException e) {
             return ResponseEntity.status(500).body("{\"error\":\"Invalid file path URL: " + e.getMessage() + "\"}");
        } catch (IOException e) {
            return ResponseEntity.status(500).body("{\"error\":\"Error reading GeoJSON file: " + e.getMessage() + "\"}");
        }
    }


    @GetMapping(value = "/postgis", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getPostgisLayerAsGeoJSON(@RequestParam String host,
                                                           @RequestParam int port,
                                                           @RequestParam String database,
                                                           @RequestParam String schema,
                                                           @RequestParam String user,
                                                           @RequestParam String password,
                                                           @RequestParam String tableName) {
        Map<String, Object> params = new HashMap<>();
        params.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
        params.put(PostgisNGDataStoreFactory.HOST.key, host);
        params.put(PostgisNGDataStoreFactory.PORT.key, port);
        params.put(PostgisNGDataStoreFactory.DATABASE.key, database);
        params.put(PostgisNGDataStoreFactory.SCHEMA.key, schema);
        params.put(PostgisNGDataStoreFactory.USER.key, user);
        params.put(PostgisNGDataStoreFactory.PASSWD.key, password);

        DataStore dataStore = null;
        try {
            dataStore = DataStoreFinder.getDataStore(params);
            if (dataStore == null) {
                return ResponseEntity.status(500).body("{\"error\":\"Could not connect to PostGIS database.\"}");
            }

            SimpleFeatureSource featureSource = dataStore.getFeatureSource(tableName);
            SimpleFeatureCollection collection = featureSource.getFeatures();

            String geoJson = convertFeatureCollectionToGeoJSON(collection);
            return ResponseEntity.ok(geoJson);

        } catch (IOException e) {
            return ResponseEntity.status(500).body("{\"error\":\"Error reading from PostGIS: " + e.getMessage() + "\"}");
        } finally {
            if (dataStore != null) {
                dataStore.dispose();
            }
        }
    }

    // 一个简单的端点,用于测试直接创建 GeoJSON FeatureCollection
    @GetMapping(value = "/sample-geojson", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getSampleGeoJSON() throws IOException {
        // 1. 定义 FeatureType (Schema)
        SimpleFeatureType featureType = DataUtilities.createType("Location",
                "geometry:Point:srid=4326," + // a geometry attribute: Point type, SRID 4326 (WGS84)
                        "name:String," + // a String attribute
                        "population:Integer" // an Integer attribute
        );

        // 2. 创建 FeatureCollection
        List<SimpleFeature> features = new ArrayList<>();
        GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
        SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);

        // 创建第一个 Feature
        Point point1 = geometryFactory.createPoint(new Coordinate(-73.985130, 40.758896)); // Times Square
        featureBuilder.add(point1);
        featureBuilder.add("Times Square");
        featureBuilder.add(10000); // 假设的人口
        SimpleFeature feature1 = featureBuilder.buildFeature("fid-1");
        features.add(feature1);

        // 创建第二个 Feature
        Point point2 = geometryFactory.createPoint(new Coordinate(-74.0060, 40.7128)); // Wall Street
        featureBuilder.add(point2);
        featureBuilder.add("Wall Street");
        featureBuilder.add(5000);
        SimpleFeature feature2 = featureBuilder.buildFeature("fid-2");
        features.add(feature2);

        SimpleFeatureCollection collection = new ListFeatureCollection(featureType, features);

        // 3. 将 FeatureCollection 转换为 GeoJSON 字符串
        String geoJson = convertFeatureCollectionToGeoJSON(collection);
        return ResponseEntity.ok(geoJson);
    }
}

解释:

  • @RestController@RequestMapping("/api/spatial") 定义了 API 的基础路径。
  • @GetMapping 定义了处理 GET 请求的端点。
  • produces = MediaType.APPLICATION_JSON_VALUE 表明端点将返回 JSON 格式的数据。
  • @RequestParam String filePath 允许你通过 URL 参数传递文件路径 (例如 http://localhost:8080/api/spatial/shapefile?filePath=/path/to/your/data.shp)。在生产环境中,直接暴露文件路径是非常不安全的,这里仅作演示。你应该使用更安全的方式来管理和访问数据文件。
  • 我们重用了之前 SpatialDataService 中读取数据的逻辑 (或者直接在控制器中实现)。
  • FeatureJSON().writeFeatureCollection(collection, writer)FeatureCollection 转换为 GeoJSON 字符串。
  • 错误处理和资源释放 :在控制器中,我们同样需要确保 DataStore 等资源被正确关闭。

运行你的 Spring Boot 应用:

在你的项目根目录下运行:

Bash 复制代码
./mvnw spring-boot:run
# 或者 (如果你使用的是 Gradle)
./gradlew bootRun

然后你可以通过浏览器或 Postman/curl 等工具访问你的 API 端点:

  • http://localhost:8080/api/spatial/shapefile?filePath=你的shapefile绝对路径.shp
  • http://localhost:8080/api/spatial/geojson-file?filePath=你的geojson绝对路径.geojson
  • http://localhost:8080/api/spatial/postgis?host=...&port=...&database=...&schema=...&user=...&password=...&tableName=...
  • http://localhost:8080/api/spatial/sample-geojson (用于测试)

4. (可选) 在前端简单展示 (例如,使用 Leaflet.js 或 OpenLayers)

这一步是可选的,但有助于你理解数据如何在前端地图上显示。我们将使用 Leaflet.js,因为它相对简单易用。

  1. 创建一个简单的 HTML 文件 (例如 src/main/resources/static/index.html):

    Spring Boot 会自动从 src/main/resources/static 或 src/main/resources/public 目录下提供静态内容。

    代码段

    html 复制代码
    <!DOCTYPE html>
    <html>
    <head>
        <title>GeoTools & Spring Boot - Leaflet Map</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
              integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
              crossorigin=""/>
        <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
                integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
                crossorigin=""></script>
        <style>
            #map { height: 600px; }
        </style>
    </head>
    <body>
    
    <div id="map"></div>
    <button onclick="loadShapefileData()">Load Shapefile Data (Sample)</button>
    <button onclick="loadGeoJSONFileData()">Load GeoJSON File Data (Sample)</button>
    <button onclick="loadSampleData()">Load Sample GeoJSON</button>
    
    <script>
        var map = L.map('map').setView([40.7128, -74.0060], 10); // 默认视图 (纽约)
    
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(map);
    
        var geoJsonLayer; // 用于存储 GeoJSON 图层,方便移除和更新
    
        function displayGeoJSON(geoJsonData) {
            if (geoJsonLayer) {
                map.removeLayer(geoJsonLayer); // 移除旧图层
            }
            geoJsonLayer = L.geoJSON(geoJsonData, {
                onEachFeature: function (feature, layer) {
                    // 尝试显示一个属性作为弹出窗口
                    if (feature.properties) {
                        let popupContent = '';
                        for (const key in feature.properties) {
                            popupContent += `<strong>${key}:</strong> ${feature.properties[key]}<br>`;
                        }
                        if (popupContent === '') {
                            popupContent = "No properties found for this feature.";
                        }
                        layer.bindPopup(popupContent);
                    }
                }
            }).addTo(map);
    
            // 缩放到图层范围
            if (geoJsonLayer.getBounds().isValid()) {
                map.fitBounds(geoJsonLayer.getBounds());
            }
        }
    
        function loadShapefileData() {
            // !!!重要!!!
            // 在生产环境中,不要直接将本地文件路径暴露给前端。
            // 这里假设你有一个本地的 Shapefile,你需要替换为你的实际路径。
            // 为了安全和方便,更好的做法是让后端API知道数据源的位置,
            // 前端只需要调用一个不带敏感路径参数的API端点。
            // 例如: /api/spatial/data/myPredefinedShapefile
            // 这里为了简单演示,我们仍然使用 filePath 参数。
            // 请将 '/path/to/your/data.shp' 替换为你的 Shapefile 文件的 *URL编码后的绝对路径*
            // 或者,将后端API修改为不需要此参数,而是从配置中读取路径。
            const filePath = prompt("Enter the absolute path to your .shp file (e.g., C:/data/my_shapefile.shp or /Users/user/data/my_shapefile.shp):");
            if (!filePath) return;
    
            fetch(`/api/spatial/shapefile?filePath=${encodeURIComponent(filePath)}`)
                .then(response => {
                    if (!response.ok) {
                        return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`) });
                    }
                    return response.json();
                })
                .then(data => {
                    console.log("Shapefile data loaded:", data);
                    displayGeoJSON(data);
                })
                .catch(error => console.error('Error loading Shapefile data:', error));
        }
    
        function loadGeoJSONFileData() {
            const filePath = prompt("Enter the absolute path to your .geojson file (e.g., C:/data/my_data.geojson or /Users/user/data/my_data.geojson):");
            if (!filePath) return;
    
            fetch(`/api/spatial/geojson-file?filePath=${encodeURIComponent(filePath)}`)
                .then(response => {
                     if (!response.ok) {
                        return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`) });
                    }
                    return response.json();
                })
                .then(data => {
                    console.log("GeoJSON file data loaded:", data);
                    displayGeoJSON(data);
                })
                .catch(error => console.error('Error loading GeoJSON file data:', error));
        }
    
    
        function loadSampleData() {
            fetch('/api/spatial/sample-geojson')
                .then(response => {
                    if (!response.ok) {
                        return response.json().then(err => { throw new Error(err.error || `HTTP error! status: ${response.status}`) });
                    }
                    return response.json();
                })
                .then(data => {
                    console.log("Sample GeoJSON data loaded:", data);
                    displayGeoJSON(data);
                })
                .catch(error => console.error('Error loading sample GeoJSON data:', error));
        }
    
        // 默认加载示例数据
        // loadSampleData();
    
    </script>
    
    </body>
    </html>
  2. 运行你的 Spring Boot 应用 (如果还没运行的话)。

  3. 在浏览器中打开 http://localhost:8080/index.html

    你应该能看到一个地图,并且可以通过点击按钮从你的 Spring Boot API 加载和显示 GeoJSON 数据。

前端代码解释:

  • 引入 Leaflet 的 CSS 和 JS 文件。

  • 创建一个 div 作为地图容器 (<div id="map"></div>)。

  • 初始化地图 (L.map('map').setView(...)) 并添加一个 OpenStreetMap 的瓦片图层。

复制代码
  displayGeoJSON(geoJsonData)

函数:

  • 如果已存在 GeoJSON 图层,则先移除。

  • 使用 L.geoJSON(geoJsonData, { onEachFeature: ... }) 将 GeoJSON 数据添加到地图上。

  • onEachFeature 允许你为每个要素执行操作,例如绑定一个包含其属性的弹出窗口 (bindPopup)。

  • map.fitBounds(...) 将地图缩放到加载的数据的范围。

  • loadShapefileData()loadGeoJSONFileData()loadSampleData() 函数:

    • 使用 fetch API 调用你的 Spring Boot 后端端点。
    • 获取到 GeoJSON 响应后,调用 displayGeoJSON 来显示数据。
    • 安全提示: loadShapefileDataloadGeoJSONFileData 中的 prompt 仅用于本地测试。在生产应用中,你不应该让前端直接指定服务器上的任意文件路径。应该设计 API,使其通过预定义的标识符或更安全的方式来获取数据。

这就完成了你的第一个 GeoTools & Spring Boot 应用!你现在已经掌握了:

  • 设置项目和依赖。
  • 使用 GeoTools 读取 Shapefile 和 GeoJSON。
  • 理解了 GeoTools 的基本要素处理概念。
  • 创建了一个 Spring Boot REST API 来提供 GeoJSON 数据。
  • (可选) 在 Leaflet 地图上显示了这些数据。

接下来,你可以探索更多 GeoTools 的功能,例如空间分析、坐标转换、数据写入、更复杂的数据库交互等等。祝你学习愉快!

相关推荐
考虑考虑5 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯6 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
青石路10 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
Java陈序员12 小时前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp
像我这样帅的人丶你还13 小时前
Java 后端详解(五):Redis 缓存
java·后端·全栈
plainGeekDev15 小时前
GreenDAO → Room
android·java·kotlin
杨运交20 小时前
[041][公共模块]分布式唯一ID生成器设计与实现:一款灵活可扩展的雪花算法框架
spring boot
亦暖筑序20 小时前
Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
java·后端
敲代码的彭于晏20 小时前
Bean 生命周期完全图解:前端同学也能看懂的 Spring 核心机制
java·前端·后端
plainGeekDev21 小时前
ButterKnife → ViewBinding
android·java·kotlin