GeoTools 实战(二):空间数据格式转换全解析——Shp ↔ GeoJSON ↔ WKT(GeoTools 29.3 + JTS 1.19.0 实战版)

目录

一、前言

在 GIS 开发中,空间数据格式转换是最常见的需求之一。不同系统、不同场景对数据格式的要求各不相同:

  • WebGIS 前端渲染:通常使用 WKT或GeoJSON
  • 永久化存储 / 专业 GIS 软件:数据库一般是Postgis或者是mysql spatial,文件一般是Shapefile,gdb
  • 接口传输 / 日志 / SQL 存储:WKT 是最简洁的文本交换格式,数据库存储一般是使用WKT字符串或者是Geometry

本文将使用 GeoTools 29.3 + JTS 1.19.0,从工程实战视角讲解四种核心格式转换场景,并重点分析其中的坑位与解决方案。


二、环境准备

Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-shapefile</artifactId>
    <version>29.3</version>
</dependency>
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-geojson</artifactId>
    <version>29.3</version>
</dependency>
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-epsg-hsql</artifactId>
    <version>29.3</version>
</dependency>
<dependency>
    <groupId>org.locationtech.jts</groupId>
    <artifactId>jts-core</artifactId>
    <version>1.19.0</version>
</dependency>

三、核心能力总览

转换方向 核心技术 典型业务场景
Shp → GeoJSON FeatureJSON + GeometryJSON WebGIS 前端渲染
GeoJSON → Shp FeatureJSON 读取 + ShapefileDataStore 写入 数据入库 / 分发
Geometry ↔ WKT WKTWriter / WKTReader 日志 / 接口 / SQL
Shp → WKT DataStore + WKTWriter 数据导出 / 人工审查

四、Shp → GeoJSON

这是最常见的转换场景,将 Shapefile 转换为 GeoJSON,便于 WebGIS 前端渲染。

核心步骤

  1. DataStoreFinder 读取 Shapefile
  2. 获取 FeatureCollection
  3. FeatureJSON 写出为 GeoJSON 格式

关键代码

java 复制代码
private static void shpToGeoJSON(String shpPath, String geojsonPath) throws Exception {
    Map<String, Serializable> params = new HashMap<>();
    params.put("url", new File(shpPath).toURI().toURL());
    params.put("charset", StandardCharsets.UTF_8.name());
    DataStore store = null;
    try {
        store = DataStoreFinder.getDataStore(params);
        String typeName = store.getTypeNames()[0];
        FeatureCollection<?, ?> collection =
                store.getFeatureSource(typeName).getFeatures();
        try (FileOutputStream fos = new FileOutputStream(geojsonPath);
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {
            FeatureJSON fjson = new FeatureJSON(new GeometryJSON(7));
            fjson.writeFeatureCollection(collection, bos);
        }
    } finally {
        if (store != null) store.dispose();
    }
}

注意事项

  • GeometryJSON(7) 中的 7 表示小数保留 7 位精度,可根据需求调整
  • DataStore 在 GeoTools 29.x 中不再实现 AutoCloseable,必须手动 dispose()

五、GeoJSON → Shapefile(重点)

这是本文最重要的一个章节,也是坑最多的场景。

坑位一:Schema 写死导致属性丢失

初期写法中,Schema 被"写死"为固定的三个字段:the_geom / name / value。这导致 GeoJSON 中的原始属性字段全部丢失。

正确做法:动态读取 GeoJSON 的 Schema,继承其属性字段结构。

坑位二:未显式绑定 CRS 导致 QGIS 无法渲染

如果不显式设置 CRS,生成的 Shapefile 可能缺失 .prj 文件或 CRS 不正确,QGIS 会提示 "要素不含几何图形"。

解决方案 :显式调用 tb.setCRS(CRS.decode("EPSG:4326"))

坑位三:3D 坐标导致写入异常

部分 GeoJSON 数据含有 Z 坐标,而 Shapefile 不支持 3D 几何。必须强制降维为 2D。

解决方案 :使用 CoordinateSequenceFilter 删除 Z 坐标。

完整代码

java 复制代码
    // ==================== GeoJSON → Shp ===================
    private static void geojsonToShp(String geojsonPath, String shpPath) throws Exception {

        try (InputStream in = new FileInputStream(geojsonPath)) {

            FeatureJSON fjson = new FeatureJSON(new GeometryJSON());
            FeatureCollection<?, ?> collection = fjson.readFeatureCollection(in);

            Map<String, Serializable> params = new HashMap<>();
            params.put(ShapefileDataStoreFactory.URLP.key, new File(shpPath).toURI().toURL());
            params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key, true);

            ShapefileDataStore ds = null;

            try {
                ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
                ds = (ShapefileDataStore) factory.createNewDataStore(params);
                ds.setCharset(StandardCharsets.UTF_8);

                // 取第一个要素,推断几何类型 + 原始属性结构
                SimpleFeature firstFeature = null;
                try (FeatureIterator<?> it = collection.features()) {
                    if (it.hasNext()) {
                        firstFeature = (SimpleFeature) it.next();
                    }
                }

                if (firstFeature == null) {
                    throw new RuntimeException("GeoJSON 中没有任何要素");
                }

                Geometry firstGeom = (Geometry) firstFeature.getDefaultGeometry();
                Class<?> geomType = firstGeom.getClass();

                // Shapefile 只支持 2D,强制降维
                if (geomType.getSimpleName().contains("3D") ||
                        firstGeom.getCoordinate().getZ() != Double.NaN) {
                    if (firstGeom instanceof Point) {
                        geomType = Point.class;
                    } else if (firstGeom instanceof LineString) {
                        geomType = LineString.class;
                    } else if (firstGeom instanceof Polygon) {
                        geomType = Polygon.class;
                    }
                }

                //  动态构建 Schema(继承 GeoJSON 的字段)
                SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
                tb.setName("layer");
                tb.setCRS(org.geotools.referencing.CRS.decode("EPSG:4326"));
                tb.add("the_geom", geomType);

                //  继承 GeoJSON 中的非几何属性字段
                SimpleFeatureType originalSchema =
                        (SimpleFeatureType) collection.getSchema();
                for (int i = 0; i < originalSchema.getAttributeCount(); i++) {
                    org.opengis.feature.type.AttributeDescriptor ad =
                            originalSchema.getDescriptor(i);
                    if (ad instanceof org.opengis.feature.type.GeometryDescriptor) {
                        continue; // 跳过几何字段
                    }
                    String attrName = ad.getLocalName();
                    Class<?> attrType = ad.getType().getBinding();
                    tb.add(attrName, attrType);
                }

                ds.createSchema(tb.buildFeatureType());

                //  写入要素(按字段名拷贝)
                try (FeatureWriter<SimpleFeatureType, SimpleFeature> writer =
                             ds.getFeatureWriterAppend(
                                     ds.getTypeNames()[0],
                                     Transaction.AUTO_COMMIT)) {

                    try (FeatureIterator<?> it = collection.features()) {
                        while (it.hasNext()) {
                            SimpleFeature from = (SimpleFeature) it.next();
                            SimpleFeature to = writer.next();

                            // 写入几何(强制 2D)
                            Geometry g = to2D((Geometry) from.getDefaultGeometry());
                            to.setAttribute("the_geom", g);

                            //  按字段名拷贝属性(不再写死 name/value)
                            for (int i = 0; i < originalSchema.getAttributeCount(); i++) {
                                org.opengis.feature.type.AttributeDescriptor ad =
                                        originalSchema.getDescriptor(i);
                                if (ad instanceof org.opengis.feature.type.GeometryDescriptor) {
                                    continue;
                                }
                                String attrName = ad.getLocalName();
                                to.setAttribute(attrName, from.getAttribute(attrName));
                            }

                            writer.write();
                        }
                    }
                }

            } finally {
                if (ds != null) {
                    ds.dispose();
                }
            }
        }

        System.out.println(" GeoJSON → Shp 完成");
    }

2D 降维工具方法

java 复制代码
    private static Geometry to2D(Geometry geom) {
        if (geom == null) return null;

        geom = geom.copy();

        geom.apply(new CoordinateSequenceFilter() {
            @Override
            public void filter(CoordinateSequence seq, int i) {
                // 强制丢弃 Z / M
                seq.setOrdinate(i, CoordinateSequence.X, seq.getX(i));
                seq.setOrdinate(i, CoordinateSequence.Y, seq.getY(i));
            }

            @Override
            public boolean isDone() {
                return false;
            }

            @Override
            public boolean isGeometryChanged() {
                return true;
            }
        });

        return geom;
    }

六、Geometry 与 WKT 互转

WKT(Well-Known Text)是 OGC 标准定义的空间对象文本表示格式,常用于接口传输、日志记录和 SQL 存储。

Geometry → WKT

java 复制代码
    private static void geometryToWKT() {
        Point point = GF.createPoint(new Coordinate(116.407, 39.904));
        WKTWriter writer = new WKTWriter();
        System.out.println(" Geometry → WKT:");
        System.out.println(writer.write(point));
    }

WKT → Geometry

java 复制代码
    private static void wktToGeometry() throws Exception {
        WKTReader reader = new WKTReader(GF);
        Geometry geom = reader.read("POINT (116.415 39.912)");
        System.out.println(" WKT → Geometry:");
        System.out.println(geom.getClass().getSimpleName() + " : " + geom);
    }

七、Shapefile → WKT 批量导出

将 Shapefile 中的每个要素的几何对象逐个转换为 WKT 格式,便于人工审查或接口传输。

java 复制代码
private static void shpToWKT(String shpPath, String wktPath) throws Exception {
        Map<String, Serializable> params = new HashMap<>();
        params.put("url", new File(shpPath).toURI().toURL());
        params.put("charset", StandardCharsets.UTF_8.name());

        DataStore store = null;
        try (PrintWriter pw = new PrintWriter(
                new OutputStreamWriter(
                        new FileOutputStream(wktPath), StandardCharsets.UTF_8))) {

            store = DataStoreFinder.getDataStore(params);
            String typeName = store.getTypeNames()[0];
            FeatureCollection<?, ?> collection =
                    store.getFeatureSource(typeName).getFeatures();

            WKTWriter wktWriter = new WKTWriter();

            try (FeatureIterator<?> it = collection.features()) {
                int idx = 0;
                while (it.hasNext()) {
                    SimpleFeature f = (SimpleFeature) it.next();
                    Geometry geom = (Geometry) f.getDefaultGeometry();
                    pw.println("# Feature " + idx++);
                    pw.println(wktWriter.write(geom));
                }
            }
        } finally {
            if (store != null) store.dispose();
        }
        System.out.println(" Shp → WKT 完成");
    }

八、完整可运行代码汇总

以下是本文所有转换功能的完整汇总,可直接复制到 IDE 运行。

java 复制代码
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.geojson.geom.GeometryJSON;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class GeoToolsFormatConvertDemo {
    //  全局 GeometryFactory(线程安全,推荐)
    private static final GeometryFactory GF = new GeometryFactory();

    public static void main(String[] args) throws Exception {

        String inputShp = "your.shp"; // 替换成你自己的shp路径

        shpToGeoJSON(inputShp, "output.geojson");
        geojsonToShp("output.geojson", "output_from_geojson.shp");
        geometryToWKT();
        wktToGeometry();
        shpToWKT(inputShp, "output.wkt");
    }


    // geojsonToShp / shpToGeoJSON / geometryToWKT /
    // wktToGeometry / shpToWKT / to2D ...
    // 方法实现请参见前文
}

运行结果

在QGIS中打开output.geojson和shp,结果如下


九、常见坑位与注意事项

坑位 原因 解决方案
DataStore 不是 AutoCloseable GeoTools 29.x 移除了该接口 使用 try/finally + dispose()
GeoJSON 属性丢失 Schema 写死为固定字段 动态读取 GeoJSON 的 Schema 继承字段
QGIS 提示无几何 未显式绑定 CRS tb.setCRS(CRS.decode("EPSG:4326"))
3D 坐标写入失败 Shapefile 不支持 Z 坐标 使用 CoordinateSequenceFilter 强制降为 2D
GeometryTransformer 不可用 JTS 1.19.0 已移除该类 使用 CoordinateSequenceFilter 替代
中文属性乱码 字符编码不一致 显式设置 UTF-8

十、工程实践建议

  1. 数据导入时动态推断几何类型,避免写死
  2. 始终显式绑定 CRS,确保生成的文件可被 QGIS / ArcGIS 正常识别
  3. GeoJSON → Shapefile 时动态继承原始属性字段,不要硬编码字段名
  4. 全局复用 GeometryFactory 实例,避免重复创建
  5. 写入 Shapefile 后及时 dispose(),释放文件锁
  6. 使用 GeometryJSON(7) 控制精度,平衡文件大小与精度

十一、小结

本文介绍了 GeoTools 中四种核心空间数据格式转换场景:Shapefile ↔ GeoJSONGeoJSON → ShapefileGeometry ↔ WKT 以及 Shapefile → WKT。通过动态 Schema 构建、显式 CRS 绑定和 2D 降维等技术,解决了 GeoJSON 转 Shapefile 过程中最常见的属性丢失和几何缺失问题。

相关推荐
小Ti客栈11 小时前
Spring Boot 集成 Springdoc-OpenAPI 与 Knife4j实现接口文档与可视化调试
java·spring boot·后端
Ai拆代码的曹操12 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
动恰客流统计12 小时前
ReID边缘计算视觉统计:餐饮店客流增长的数字化破局路径
java·大数据·运维·人工智能
Ivanqhz13 小时前
Rust &‘static str浅析
java·前端·javascript·rust
weixin_4196583115 小时前
Docker 搭建 Jenkins 服务
java·docker·jenkins
captain37616 小时前
多线程线程安全问题
java·java-ee
CoderYanger16 小时前
A.每日一题:1979. 找出数组的最大公约数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
灵极海16 小时前
LangChain4j RAG 实战完整指南:从入门到踩坑
java·langchain
yaoxin52112316 小时前
476. Java 反射 - 调用方法
java·开发语言
Ivanqhz17 小时前
Rust Lazy浅析
java·javascript·rust