geotools 是 java 生态下处理空间数据的开源代码库,webgis 开发中常用的 geoserver 也使用了 geotools 作为底层库。
当你有处理空间数据的需求,你可能就需要用到 geotools 了。比如读取地理数据, 读取解析 shpfile、将地理数据写入数据库;地理数据转换:比如说将 shpfile 转换为 geojson 等其他格式。
1、核心特性
- 一组简洁的数据 api,包括多种格式的空间数据和数据库、坐标系统转换、地图投影、空间数据分析和过滤
- 服务端地图渲染
- 基于 XML 的数据解析和转化,支持多种 OGC 标准(GML, Filter, KML, SLD, and SE.)
- GeoTools Plugins:提供开放的插件系统,
- GeoTools Extensions
- 不支持的内容
2、实现的标准
- OGC Style Layer Descriptor / Symbology Encoding data structures and rendering engine
- OGC General Feature Model including Simple Feature support
- OGC Grid Coverage representation of raster information
- OGC Filter and Common Constraint Language (CQL)
- Clients for Web Feature Service, Web Map Service and experimental support for Web Process Service
- ISO 19107 Geometry
3、支持的数据格式
栅格数据:
arcgridgeotiffgrassraster- image (
JPEGTIFFGIFPNG), imageio-ext-gdalimagemosaicimagepyramidJP2Kmatlab
数据库支持:
db2geopackagehanah2mysqloraclepostgissqlserverteradata
矢量数据:
app-schemacsvgeojsonpropertyshapefilewfs
XML Bindings:
Java data structures and bindings provided for the following:
xsd-core(xml simple types)fesfiltergml2gml3kmlowssldwcswfswmswmtswps
4、geotools 源码架构
Geotools 源码结构采用模块化的结构,整体包含核心库、 extension 和 plugins 两部分。
library 是 geotools 核心模块,实现了地理数据定义、图形定义、;
plugin 采用 java spi 机制;
extension 基于核心库实现了额外的功能;
4.1、GeoTools 架构概述
- 模块化设计:GeoTools 是一个分层的软件“堆栈”,每个模块基于前面模块的概念构建。
- 核心模块:
gt-api:定义空间概念的接口。gt-metadata:实现标识和描述功能。gt-referencing:实现坐标定位和转换。gt-main:提供数据 API 和默认实现(如过滤器、特征等)。gt-render:提供地图 API 和 Java2D 渲染引擎。gt-jdbc:实现对空间数据库的访问。gt-xml:实现常见空间 XML 格式。gt-cql:实现通用查询语言用于过滤。gt-coverage:实现对栅格信息的访问。
- 依赖关系:某些模块需要依赖其他模块才能正常工作。例如,使用
gt-referencing模块时,需要gt-api、gt-metadata和gt-referencing,并且需要一个插件(如gt-epsg-hsql)来提供 EPSG 定义。
4.2、插件
- 功能扩展:GeoTools 提供插件来支持额外的数据格式、不同的坐标参考系统等。
- 常见插件:
- 数据库支持(如 MySQL、PostGIS、SQL Server 等)。
- 栅格格式支持(如 GeoTIFF、arcgrid 等)。
- EPSG 数据库支持(如 Access、HSQL、PostgreSQL 等)。
4.3、GeoTools 扩展
- 基于核心库的扩展:提供额外功能,如:
gt-app-schema:将应用程序模式映射到复杂特征模型。gt-brewer:使用颜色调色板生成样式。gt-wms和gt-wmts:Web 地图服务器和 Web 地图瓦片服务器客户端。gt-xsd:解析和编码常见 OGC 模式。
4.4、XML 模块支持
- XML 模式打包:提供常见的 XML 模式(如 OGC 模式)的 JAR 包,避免每次从互联网下载。
- XSD 插件:通过 Eclipse XSD 库解析和编码 XML 模式文档,支持如 GML、WFS、WMS 等格式。
4.5、不支持的模块
未包含在 GeoTools 的一些模块(如 gt-swt、gt-swing、gt-wps 等)未包含在官方下载中,但可通过 Maven 或单独下载。
5、快速开始
5.1、maven 引入
本文我们引入gt-shapefile、gt-epsg-hsql、gt-geotiff这几个包
gt-shapefile 包依赖 gt-main,所以我们不需单独引入
<dependencies>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geotiff</artifactId>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
<!--这里需要注意geotools的包在maven中央仓库是下载不到的,需要添加以下仓库-->
<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>
5.2、操作 shp
我们首先来看看读取 shp 的代码,主要用到 DataStore 这个核心类:
File file = new File("example.shp");
Map<String, Object> map = new HashMap<>();
map.put("url", file.toURI().toURL());
// 使用
DataStore dataStore = DataStoreFinder.getDataStore(map);
String typeName = dataStore.getTypeNames()[0];
FeatureSource<SimpleFeatureType, SimpleFeature> source =
dataStore.getFeatureSource(typeName);
FeatureCollection<SimpleFeatureType,
SimpleFeature> collection = source.getFeatures();
// 遍历feature
try (FeatureIterator<SimpleFeature> features = collection.features()) {
while (features.hasNext()) {
SimpleFeature feature = features.next();
System.out.print(feature.getID());
System.out.print(": ");
System.out.println(feature.getDefaultGeometryProperty().getValue());
}
}
上述代码中有几个需要了解的关键类(概念):
datastore: 是一个入口,用于访问和存储矢量空间数据。(可以理解为一个 shpfile 文件)包含 shpfile、数据库以及其他格式。通过 datastore 的 api,我们可以访问空间数据,比如getFeatureSource。- DataStoreFinder:Geotools 提供了多种类型的 DataStore,比如 ShapeFileDataStore、posigisDataStore, DataStoreFinder会根据请求参数自动帮我们构造最合适的 DataStore。
- FeatureSource: 具体持有空间数据内容(可以理解为shp 文件的具体内容)
- feature: 具体的矢量要素(可以理解为一份 Point 类型 shp 数据中的一个点),包含地理数据和属性数据,支持嵌套属性和复杂的数据类型,更灵活但也更复杂,实际中更多使用 simpleFeature;
- SimpleFeature: 简化版本的 feature,不支持嵌套属性,,提供更便捷的 APi 。我们实际接触的绝大多数地理数据都可以用 SimpleFeature 表示,大多数应用场景下使用SimpleFeature就足够了,
5.2.1、shp 乱码问题
在使用 geotools 处理 shpfile 文件时,有时会碰到乱码的问题。
通常我们用到的 shp 文件主要是由 arcgis 制作的,在 10.2.1 以后的版本,arcgis 默认使用的是 utf-8 编码,而在之前的版本中使用gbk编码。
shpfile 文件中.cpg文件中存储的该 shp 文件原始的编码信息,而 geotools 在读取 shpfile 文件时,并不会默认读取这个文件中的编码,而是设置了一个默认的编码iso-8859-1,此时需要我们手动设置编码。
但这里并不是简单的设置为 gbk 或 utf-8 就可以了,如果原始数据是 utf-8 编码,而我们设置为了 gbk 编码,同样也会出现乱码。
所以,推荐这样处理:
// 读取cpg文件
Optional<File> cpgFile = Files.walk(zipPath)
.map(Path::toFile)
.filter(file -> file.getName().toLowerCase().endsWith(".cpg"))
.findFirst();
ShapefileDataStore dataStore = new ShapefileDataStore(shpFile.toURI().toURL());
// 读取shp原始数据的cpg文件中的编码
if (cpgFile.isPresent()) {
String cpgContent = Files.readString(cpgFile.get().toPath());
// 根据cpg内容设置ShapefileDataStore的编码
dataStore.setCharset(Charset.forName(cpgContent));
} else {
dataStore.setCharset(StandardCharsets.UTF_8);
}
5.2.2、写入 shp
接下来我们看看如何使用 geotools 创建空间要素并写入 shp。
创建要素首先要创建地理要素类型 featureType,相当于先创建数据库数据表结构,在往里边填数据。
// 使用geotools提供的SimpleFeatureTypeBuilder
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName("Location");
// 设置坐标系 设
builder.setCRS(org.geotools.referencing.crs.DefaultGeographicCRS.WGS84); // 设置坐标系
// 添加属性,分别为属性名和值类型
builder.add("the_geom", Point.class);
builder.add("name", String.class);
builder.add("number", Integer.class);
// 创建出类型
SimpleFeatureType TYPE = builder.buildFeatureType();
创建出featureType后,我们就可以创建地理要素了:
// 简单要素构造器
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
// 创建geometry factory
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
// 创建点要素
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
// 添加属性,注意添加顺序要与eatureType中定义的匹配,否则会通不过校验
featureBuilder.add(point);
featureBuilder.add("name");
featureBuilder.add(1);
// 也可以通过set(key,value)的方式添加
// featureBuilder.set("the_geom",point);
// 构造要素,传入的参数1 为要素 id
SimpleFeature feature = featureBuilder.buildFeature("1");
List<SimpleFeature> features = new ArrayList<>();
features.add(feature)
构造好地理要素 feature 后,我们就可以写入 shpfile 了:
// 指定输出文件目录
File file = new File("example.shp");
ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
Map<String, Serializable> params = new HashMap<>();
params.put("url", file.toURI().toURL());
params.put("create spatial index", Boolean.TRUE);
ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
// shp文件内容的模板
newDataStore.createSchema(TYPE);
Transaction transaction = new DefaultTransaction("create");
// 获取shp文件名,返回的是cities(自动取出.shp拓展名)
String typeName = newDataStore.getTypeNames()[0];
// 根据typeName 获取 shp文件内容
SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);
// 写入文件
// 使用instanof 判断文件是否可写入, 确认FeatureSource 对象实现了FeatureStore 方法
if (featureSource instanceof SimpleFeatureStore) {
SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;
featureStore.setTransaction(transaction);
try {
featureStore.addFeatures(DataUtilities.collection(features));
// 使用 transaction.commit安全地写出
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
transaction.rollback();
} finally {
transaction.close();
}
}
我们来看下完整代码:
package org.example;
import junit.framework.TestCase;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
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.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TestShapefile extends TestCase {
public void testCreateShpfile() {
try {
// 1. 定义要素类型
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName("Location");
// 设置坐标系
builder.setCRS(org.geotools.referencing.crs.DefaultGeographicCRS.WGS84);
// 添加属性
builder.add("the_geom", Point.class);
builder.add("name", String.class);
builder.add("number", Integer.class);
SimpleFeatureType TYPE = builder.buildFeatureType();
// 2. 准备要素数据
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
// 创建点要素
Point point = geometryFactory.createPoint(new Coordinate(105.0, 24.0));
// 添加属性,注意添加顺序要与eatureType中定义的匹配,否则会通不过校验
featureBuilder.add(point);
featureBuilder.add("name");
featureBuilder.add(1);
SimpleFeature feature = featureBuilder.buildFeature("1");
List<SimpleFeature> features = new ArrayList<>();
features.add(feature);
// 3. 创建 Shapefile
File file = new File("example.shp");
ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
Map<String, Serializable> params = new HashMap<>();
params.put("url", file.toURI().toURL());
params.put("create spatial index", Boolean.TRUE);
ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);
newDataStore.createSchema(TYPE);
// 4. 写入数据
Transaction transaction = new DefaultTransaction("create");
String typeName = newDataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName);
if (featureSource instanceof SimpleFeatureStore) {
SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource;
featureStore.setTransaction(transaction);
try {
featureStore.addFeatures(DataUtilities.collection(features));
transaction.commit();
} catch (Exception e) {
e.printStackTrace();
transaction.rollback();
} finally {
transaction.close();
}
}
System.out.println("Shapefile 创建成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void addPoint(List<SimpleFeature> features,
SimpleFeatureBuilder featureBuilder,
GeometryFactory geometryFactory,
double longitude,
double latitude,
String name,
int number) {
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
featureBuilder.add(point);
featureBuilder.set("the_geom", point);
featureBuilder.add(name);
featureBuilder.add(number);
SimpleFeature feature = featureBuilder.buildFeature(null);
features.add(feature);
}
}
执行程序,可以看到文件成功导出:
我们导入 qgis 看下,属性和坐标都有:
5.2.3、重投影(坐标系转换)
我们在业务开发中也经常会有对数据进行重投影的需求,geotools 也提供了支持,
首先我们需要引入gt-epsg-hsql包,这个包是基于 hsql 这个内存数据库,存储了一些地理编码:
<dependencies>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
</dependencies>
</repositories>
坐标转换的核心代码如下:
- 通过 CRS.decode 方法获取坐标系,
- CRS.findMathTransform 方法可以获取坐标系转换的参数
- JTS.transform 方法对一个 geometry 进行坐标转换,返回转换后的 geometry
import org.geotools.referencing.CRS;
import org.geotools.geometry.jts.JTS;
import org.locationtech.jts.geom.Geometry;
//......
CoordinateReferenceSystem originCRS = CRS.decode("EPSG:4326");
CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:3005");
MathTransform transform = CRS.findMathTransform(dataCRS, targetCRS,true);
Geometry geometry2 = JTS.transform(geometry, transform);
接下来我们来看下如何对一个 shpfile 文件进行重投影,并将数据重新导出为新的 shpfile 的完整代码;
package org.example;
import junit.framework.TestCase;
import org.geotools.data.*;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class TestProjection extends TestCase {
public void testProjection() throws IOException, FactoryException {
// 读取原始datastore
URL path = getClass().getResource("/shapefile/example.shp");
File file = new File(path.getFile());
Map<String, Object> maps = new HashMap<>();
maps.put("url", file.toURI().toURL());
DataStore dataStore = DataStoreFinder.getDataStore(maps);
String typeName = dataStore.getTypeNames()[0];
FeatureSource<SimpleFeatureType, SimpleFeature> featureSource =
dataStore.getFeatureSource(typeName);
FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection = featureSource.getFeatures();
SimpleFeatureType schema = featureSource.getSchema();
// 获取坐标转换矩阵
CoordinateReferenceSystem dataCRS = schema.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:3005");
MathTransform transform = CRS.findMathTransform(dataCRS, targetCRS,true);
// 写入新的shpfile
File newFile = new File("example_reprojection.shp");
DataStore dataStore1 = new ShapefileDataStore(newFile.toURI().toURL());
SimpleFeatureType featureType = SimpleFeatureTypeBuilder.retype(schema, targetCRS);
dataStore1.createSchema(featureType);
String createdName = dataStore1.getTypeNames()[0];
// 进行坐标投影转换
Transaction transaction = new DefaultTransaction("Reproject");
try (FeatureWriter<SimpleFeatureType, SimpleFeature> writer =
dataStore1.getFeatureWriterAppend(createdName, transaction);
SimpleFeatureIterator iterator = (SimpleFeatureIterator) featureCollection.features()) {
while (iterator.hasNext()) {
// copy the contents of each feature and transform the geometry
SimpleFeature feature = iterator.next();
SimpleFeature copy = writer.next();
copy.setAttributes(feature.getAttributes());
Geometry geometry = (Geometry) feature.getDefaultGeometry();
Geometry geometry2 = JTS.transform(geometry, transform);
copy.setDefaultGeometry(geometry2);
writer.write();
}
transaction.commit();
} catch (Exception problem) {
problem.printStackTrace();
transaction.rollback();
} finally {
transaction.close();
}
}
}
5.3、栅格数据处理
GeoTools是通过gt-coverage模块来管理栅格数据的。
引入 gt-coverage 模块
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-coverage</artifactId>
<version>${geotools.version}</version>
</dependency>
为了管理栅格,首先需要定义栅格数据的对象。GridCoverage是GeoTools封装的栅格对象类,根据栅格文件扩展名的不同,GeoTools会自动调用对应的读写实现,泛式创建GridCoverage的方法
我们可以通过GridFormatFinder来泛式创建一个GridCoverage:
File file = new File("test.tiff");
AbstractGridFormat format = GridFormatFinder.findFormat(file);
GridCoverage2DReader reader = format.getReader(file);
GridCoverage2D coverage = reader.read(null);
如果我们明确知道我们要处理的栅格数据格式,我们也可以直接创建,比如对于最常使用的 geotiff:
File file = new File("test.tiff");
GeoTiffReader reader = new GeoTiffReader(file, new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE));
GridCoverage2D coverage = reader.read(null);
GridCoverage 最常用的功能就是计算指定地理位置的栅格像素值:
GridCoverage2D coverage = reader.read(null);
DirectPosition position = new DirectPosition2D(crs, x, y);
double[] sample = (double[]) coverage.evaluate(position); // 假定为双精度浮点型数组
// 执行重采样操作
sample = coverage.evaluate(position, sample);
- Envelope:在 GeoTools 中,Envelope 是一个用于描述数据在地理空间中“边界框”的概念。
5.3.1、栅格图像处理
GeoTools提供了一个Operations工具集,该工具集包含了一些高级图像处理工具,用户仅需输入指定的参数即可运行。GeoTools提供一个静态工具类Operations.DEFAULT,用于实现默认的参数输入和配置。
(1)实例化一个Processor对象。(2)获取图像处理工具Operations的输入参数列表。(3)对输入参数进行校验。(4)调用Processor对象的doOperation方法,在此方法中还支持其他高级处理配置。
PS: 关于栅格数据重采样修改栅格分辨率的代码,在官网和源码中没有找到(可能是我没理解吧)
5.3.2、坐标转换
CoordinateReferenceSystem targetCRS =
CRS.decode("EPSG:32632");
GridCoverage2D covtransformed =
(GridCoverage2D) Operations.DEFAULT.resample(scicov,targetCRS);
5.3.3、栅格裁剪
final AbstractProcessor processor = new DefaultProcessor(null);
final ParameterValueGroup param = processor.getOperation("CoverageCrop").getParameters();
GridCoverage2D coverage = ...{get a coverage from somewhere}...;
final GeneralEnvelope crop = new GeneralEnvelope( ... );
param.parameter("Source").setValue( coverage );
param.parameter("Envelope").setValue( crop );
GridCoverage2D cropped = (GridCoverage2D) processor.doOperation(param);