GeoTools 是一个开源的 Java GIS 工具包 ,旨在提供符合 开放地理空间联盟(OGC)规范 的地理空间数据处理、访问和可视化方法,其官方网站为 geotools.org 。作为开源地理信息领域的核心类库,它被广泛用于构建 Web 服务、命令行工具及桌面应用程序,支持多种数据格式与空间数据库的交互 。
核心功能与技术特性
GeoTools 的技术特性主要体现在对国际标准的全面支持、模块化架构以及丰富的空间数据处理能力上,能够满足从数据存取到地图渲染的多种开发需求。
- 标准规范支持 :GeoTools 完整遵循并实现了 OGC 标准,包括网络地图服务(WMS)、网络要素服务(WFS)、地理标记语言(GML)、样式化图层描述符(SLD)等,确保不同地理信息系统间的数据共享与互操作性 。1百科
- 数据处理能力 :提供了一套干净、统一的数据访问 API(DataStore),支持读写多种 矢量数据格式 (如 Shapefile、GeoJSON、CSV、DXF)和 栅格数据格式 (如 GeoTIFF、JPEG、PNG),并能连接主流 空间数据库(如 PostGIS、Oracle、MySQL、SQL Server) 。
- 坐标与渲染引擎 :内置强大的 坐标参考系统(CRS)支持,能够处理复杂的地图投影并执行精确的坐标转换;同时提供高效、无状态且低内存消耗的地图渲染引擎,适合服务器端环境,支持使用 SLD 定义复杂的地图样式 。1百科5
- 依赖与架构 :基于 Java 拓扑套件(JTS) 定义空间数据结构,并依赖 GeoAPI 接口规范;采用模块化架构,各组件间松散耦合,允许开发者灵活选用所需功能或通过插件系统扩展新的数据格式支持
应用场景与生态系统
GeoTools 经过二十多年的发展,已形成活跃的开源社区生态,构成了从桌面应用到云端服务的广泛技术基础,适用于多种地理信息系统开发场景。
- 基础组件支撑 :作为核心底层库,GeoTools 是多个知名开源 GIS 项目的基础,例如 GeoServer 使用其作为核心数据访问和处理引擎,uDig 是基于其构建的开源桌面 GIS 客户端,GeoMesa 则通过扩展其数据源能力来处理大规模时空数据 。1百科7
- WebGIS 开发:广泛用于构建互联网上的 GIS 应用软件,通过实现 OGC 网络服务标准,支持地图服务的快速发布、矢量数据的自适应渲染以及空间数据的在线查询与分析 。78
- 行业解决方案:在电力、物流、城市规划等领域有具体应用,如电网空间信息呈现、配送路线管理系统、城市积水面实时生成与可视化、等高线批量赋值等,帮助解决数据解析、坐标计算及空间关系分析等常见问题 。78
- 大数据与云存储:支持与非结构化数据库(如 HBase)结合,提出矢量空间数据存储模型,解决云存储技术中缺乏空间数据拓扑关系描述的问题,适用于海量地理空间数据的管理 。8
开发资源与架构说明
对于有志于从事地理信息系统开发的人员,GeoTools 提供了丰富的文档与社区支持,但在使用时需注意其特定的技术限制与依赖关系。
- 开源许可与维护 :项目基于 GNU 宽通用公共许可证(LGPL) 发布,由开源社区维护,隶属于 **开源地理空间基金会(OSGeo)** 旗下项目,拥有活跃的贡献者与项目管理委员会 。
- 技术局限性 :目前 GeoTools 主要基于 2D 图形,缺乏对 3D 空间数据算法和显示的原生支持;在使用时需注意其与 JTS 和 GeoAPI 的代码实现差异,有时需要进行相互转化 。
- 获取与集成:开发者可通过 Maven 构建系统引入相关 jar 包,利用 DataStoreFinder 动态加载数据存储,并通过 CQL(公共查询语言)创建过滤器进行空间查询;官方文档与 OSGeo Live 应用程序提供了详细的入门指南与概述 。
这里使用的Geotools的版本为
<geotools.version>34.1</geotools.version>
需要加入依赖:
<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-geojson</artifactId>
<version>${geotools.version}</version>
</dependency>
GeoTools在Maven的镜像中可能不存在,需要在pom.xml中加入GeoTools的仓库
<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>
或者在settings.xml加入:
<!-- 阿里云镜像配置 -->
<mirrors>
<mirror>
<id>aliyunmaven</id>
<name>阿里云中央仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>aliyunmaven-spring</id>
<name>阿里云Spring仓库</name>
<url>https://maven.aliyun.com/repository/spring</url>
<mirrorOf>spring-plugin</mirrorOf>
</mirror>
<mirror>
<id>aliyunmaven-google</id>
<name>阿里云Google仓库</name>
<url>https://maven.aliyun.com/repository/google</url>
<mirrorOf>google</mirrorOf>
</mirror>
<!-- osgeo(伪)镜像 -->
<mirror>
<id>mirror-osgeo</id>
<mirrorOf>osgeo</mirrorOf>
<name>Mirror Osgeo</name>
<url>https://repo.osgeo.org/repository/release/</url>
</mirror>
</mirrors>
Java代码:
package com.haifang.utils;
/**
* @author chenshixian
**/
import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.geotools.api.data.Transaction;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.data.SimpleFeatureStore;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.locationtech.jts.geom.Geometry;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class GeoJsonToShpConverter {
private static final CoordinateReferenceSystem DEFAULT_CRS = DefaultGeographicCRS.WGS84;
private static final Charset SHAPEFILE_CHARSET = StandardCharsets.UTF_8;
private static final String GEOMETRY_FIELD_NAME = "the_geom"; // 标准几何字段名
private static final FeatureJSON featureJSON = new FeatureJSON();
public static void main(String[] args) throws Exception {
String[] geojsonList = new String[]{"output/ships_trajectory.geojson", "output/ships_points.geojson"};
for(String path: geojsonList) {
// 输入GeoJSON文件路径
File inputFile = new File(path);
// 输出目录
File outputDir = new File(inputFile.getParentFile(), "shp");
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// 读取并转换GeoJSON
convertGeoJsonToShapefiles(inputFile, outputDir);
}
}
/**
* 将GeoJSON文件转换为多个Shapefile
*/
public static void convertGeoJsonToShapefiles(File geoJsonFile, File outputDir) throws Exception {
// 读取GeoJSON内容
String geoJsonContent = FileUtil.readUtf8String(geoJsonFile);
JSONObject geoJsonObject = JSONObject.parseObject(geoJsonContent, Feature.OrderedField);
JSONArray features = geoJsonObject.getJSONArray("features");
if (features == null || features.isEmpty()) {
System.out.println("GeoJSON文件中没有要素数据");
return;
}
// 按几何类型分组
Map<String, List<SimpleFeature>> geometryTypeMap = new HashMap<>();
Map<String, Set<String>> allProperties = new HashMap<>();
// 先收集所有要素和属性
for (int i = 0; i < features.size(); i++) {
JSONObject featureJson = features.getJSONObject(i);
JSONObject geometry = featureJson.getJSONObject("geometry");
String geometryType = geometry.getString("type");
// 修复只有一个点的LineString
if ("LineString".equals(geometryType)) {
JSONArray coordinates = geometry.getJSONArray("coordinates");
if (coordinates != null && coordinates.size() == 1) {
// 复制该点,使LineString有两个相同的点
coordinates.add(coordinates.getJSONArray(0));
}
}
// 转换为SimpleFeature
String featureStr = featureJson.toJSONString();
SimpleFeature feature = featureJSON.readFeature(new StringReader(featureStr));
// 添加到对应类型的列表
List<SimpleFeature> typeList = geometryTypeMap.computeIfAbsent(geometryType,
k -> new ArrayList<>());
typeList.add(feature);
// 收集属性字段
JSONObject properties = featureJson.getJSONObject("properties");
if (properties != null) {
Set<String> props = allProperties.computeIfAbsent(geometryType,
k -> new HashSet<>());
props.addAll(properties.keySet());
}
}
// 获取文件名前缀
String fileNamePrefix = FileUtil.getPrefix(geoJsonFile);
// 为每种几何类型创建Shapefile
for (Map.Entry<String, List<SimpleFeature>> entry : geometryTypeMap.entrySet()) {
String geometryType = entry.getKey();
List<SimpleFeature> featureList = entry.getValue();
// 获取该类型的所有属性字段
Set<String> propertiesSet = allProperties.getOrDefault(geometryType, new HashSet<>());
// 创建Shapefile
File shpFile = new File(outputDir, fileNamePrefix + "_" + geometryType.toLowerCase() + ".shp");
createShapefile(shpFile, featureList, geometryType, new ArrayList<>(propertiesSet));
System.out.println("已创建: " + shpFile.getName() +
", 包含 " + featureList.size() + " 个要素");
}
}
/**
* 创建Shapefile
*/
private static void createShapefile(File shpFile,
List<SimpleFeature> features,
String geometryType,
List<String> propertyFields) throws Exception {
// 创建要素类型
SimpleFeatureType featureType = createFeatureType(shpFile, geometryType, propertyFields);
// 创建FeatureCollection
DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);
for (int i = 0; i < features.size(); i++) {
SimpleFeature originalFeature = features.get(i);
// 设置几何字段
Geometry geometry = (Geometry) originalFeature.getDefaultGeometry();
featureBuilder.add(geometry);
// 设置属性字段
for (String fieldName : propertyFields) {
Object value = originalFeature.getAttribute(fieldName);
if (value == null) {
// 尝试从properties中获取
Object properties = originalFeature.getAttribute("properties");
if (properties instanceof Map) {
value = ((Map<?, ?>) properties).get(fieldName);
}
}
featureBuilder.add(value != null ? value.toString() : null);
}
// 设置FID
featureBuilder.add("FID_" + (i + 1));
// 构建要素并添加到集合
SimpleFeature feature = featureBuilder.buildFeature(null);
featureCollection.add(feature);
}
// 写入Shapefile
writeToShapefile(shpFile, featureCollection, featureType);
createCpgFile(shpFile);
}
private static void createCpgFile(File shpFile) throws IOException {
String basePath = shpFile.getAbsolutePath();
String baseName = basePath.substring(0, basePath.lastIndexOf('.'));
File cpgFile = new File(baseName + ".cpg");
try (FileOutputStream fos = new FileOutputStream(cpgFile)) {
String charsetName = SHAPEFILE_CHARSET.name();
fos.write(charsetName.getBytes(StandardCharsets.US_ASCII));
}
System.out.println(" 已创建CPG文件: " + cpgFile.getName() + " (编码: " + SHAPEFILE_CHARSET.name() + ")");
}
/**
* 创建要素类型
*/
private static SimpleFeatureType createFeatureType(File shpFile,
String geometryType,
List<String> propertyFields) throws SchemaException {
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName(FileUtil.getPrefix(shpFile));
builder.setCRS(DEFAULT_CRS);
// 添加几何字段
builder.add(GEOMETRY_FIELD_NAME, getGeometryClass(geometryType));
builder.setDefaultGeometry(GEOMETRY_FIELD_NAME);
// 添加属性字段
for (String fieldName : propertyFields) {
// 处理字段名长度限制(DBF字段名限制为10个字符)
String dbfFieldName = truncateFieldName(fieldName);
builder.add(dbfFieldName, String.class);
}
// 添加ID字段
builder.add("fid", String.class);
return builder.buildFeatureType();
}
/**
* 根据几何类型获取对应的Geometry类
*/
private static Class<?> getGeometryClass(String geometryType) {
switch (geometryType.toUpperCase()) {
case "POINT":
return org.locationtech.jts.geom.Point.class;
case "LINESTRING":
return org.locationtech.jts.geom.LineString.class;
case "POLYGON":
return org.locationtech.jts.geom.Polygon.class;
case "MULTIPOINT":
return org.locationtech.jts.geom.MultiPoint.class;
case "MULTILINESTRING":
return org.locationtech.jts.geom.MultiLineString.class;
case "MULTIPOLYGON":
return org.locationtech.jts.geom.MultiPolygon.class;
case "GEOMETRYCOLLECTION":
return org.locationtech.jts.geom.GeometryCollection.class;
default:
throw new IllegalArgumentException("不支持的几何类型: " + geometryType);
}
}
/**
* 截断字段名以适应DBF文件格式限制(最大10个字符)
*/
private static String truncateFieldName(String fieldName) {
if (fieldName == null || fieldName.length() <= 10) {
return fieldName;
}
// 保留字段名的前10个字符
return fieldName.substring(0, 10);
}
/**
* 将FeatureCollection写入Shapefile
*/
private static void writeToShapefile(File shpFile,
DefaultFeatureCollection featureCollection,
SimpleFeatureType featureType) throws IOException {
ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
Map<String, Serializable> params = new HashMap<>();
params.put("url", shpFile.toURI().toURL());
params.put("create spatial index", Boolean.TRUE);
ShapefileDataStore newDataStore = null;
try {
// 创建新的数据存储
newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
newDataStore.createSchema(featureType);
newDataStore.setCharset(StandardCharsets.UTF_8);
// 获取类型名称
String typeName = newDataStore.getTypeNames()[0];
// 获取FeatureSource
SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);
if (featureSource instanceof SimpleFeatureStore) {
SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;
// 设置事务
Transaction transaction = new DefaultTransaction("create");
featureStore.setTransaction(transaction);
try {
// 写入要素
featureStore.addFeatures(featureCollection);
transaction.commit();
System.out.println("成功写入: " + featureCollection.size() + " 个要素到 " + shpFile.getName());
} catch (Exception e) {
transaction.rollback();
throw e;
} finally {
transaction.close();
}
}
} finally {
// 确保数据存储被正确关闭和清理
if (newDataStore != null) {
newDataStore.dispose();
}
}
}
}