🌍geotools入门

2,779 阅读6分钟

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、支持的数据格式

栅格数据:

  • arcgrid
  • geotiff
  • grassraster
  • image ( JPEG TIFF GIF PNG ),
  • imageio-ext-gdal
  • imagemosaic
  • imagepyramid
  • JP2K
  • matlab

数据库支持:

  • db2
  • geopackage
  • hana
  • h2
  • mysql
  • oracle
  • postgis
  • sqlserver
  • teradata

矢量数据:

  • app-schema
  • csv
  • geojson
  • property
  • shapefile
  • wfs

XML Bindings:

Java data structures and bindings provided for the following:

  • xsd-core (xml simple types)
  • fes
  • filter
  • gml2
  • gml3
  • kml
  • ows
  • sld
  • wcs
  • wfs
  • wms
  • wmts
  • wps

4、geotools 源码架构

docs.geotools.org/latest/user…

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-apigt-metadatagt-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-wmsgt-wmts:Web 地图服务器和 Web 地图瓦片服务器客户端。
    • gt-xsd:解析和编码常见 OGC 模式。

4.4、XML 模块支持

  • XML 模式打包:提供常见的 XML 模式(如 OGC 模式)的 JAR 包,避免每次从互联网下载。
  • XSD 插件:通过 Eclipse XSD 库解析和编码 XML 模式文档,支持如 GML、WFS、WMS 等格式。

4.5、不支持的模块

未包含在 GeoTools 的一些模块(如 gt-swtgt-swinggt-wps 等)未包含在官方下载中,但可通过 Maven 或单独下载。

5、快速开始

5.1、maven 引入

本文我们引入gt-shapefilegt-epsg-hsqlgt-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);