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
实例的工厂接口。例如,ShapefileDataStoreFactory
、GeoJSONDataStoreFactory
、JDBCDataStoreFactory
(具体到 PostGIS 是PostGISDialectFactory
与JDBCDataStoreFactory
结合使用)。FeatureSource
: 一旦你有了DataStore
,你可以通过它获取FeatureSource
。FeatureSource
代表了一层地理要素 (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:
- 使用
GeoJSONDataStoreFactory
: 这种方式与其他DataStore
类似,更通用。 - 直接使用
gt-geojson
模块 (例如GeoJSONReader
或FeatureJSON
) : 这种方式更直接,有时更简单,特别是当你只需要读取 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。
你需要:
- PostGIS (或其他空间数据库) 的 JDBC驱动 (例如
postgresql
驱动)。 - 数据库连接参数 (主机, 端口, 数据库名, 用户名, 密码等)。
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,因为它相对简单易用。
-
创建一个简单的 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/[email protected]/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <script src="https://unpkg.com/[email protected]/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: '© <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>
-
运行你的 Spring Boot 应用 (如果还没运行的话)。
-
在浏览器中打开
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
来显示数据。 - 安全提示:
loadShapefileData
和loadGeoJSONFileData
中的prompt
仅用于本地测试。在生产应用中,你不应该让前端直接指定服务器上的任意文件路径。应该设计 API,使其通过预定义的标识符或更安全的方式来获取数据。
- 使用
这就完成了你的第一个 GeoTools & Spring Boot 应用!你现在已经掌握了:
- 设置项目和依赖。
- 使用 GeoTools 读取 Shapefile 和 GeoJSON。
- 理解了 GeoTools 的基本要素处理概念。
- 创建了一个 Spring Boot REST API 来提供 GeoJSON 数据。
- (可选) 在 Leaflet 地图上显示了这些数据。
接下来,你可以探索更多 GeoTools 的功能,例如空间分析、坐标转换、数据写入、更复杂的数据库交互等等。祝你学习愉快!