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 过程中最常见的属性丢失和几何缺失问题。

相关推荐
SL-staff1 小时前
智慧园区2000+设备统一管理:JVS-IoT如何降低运维成本40%
java·开发语言·物联网·智慧园区·设备管理·运维优化·jvs-iot
咖啡八杯2 小时前
GoF设计模式——模板方法模式
java·后端·spring·设计模式
爱笑的源码基地3 小时前
一款全开源的信创云PACS影像云平台解决方案,支持多模态医学影像处理(CT/MR/DR/超声/病理等)
java·源码·国产化·pacs·云影像·区域pacs
我命由我123453 小时前
Android 开发问题:ClickableSpan 的点击事件没有生效
java·java-ee·android studio·android jetpack·android-studio·android runtime
万亿少女的梦1683 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
微信开发api-视频号协议3 小时前
企业微信外部群开发自动化实践过程
java·前端·微信·自动化·企业微信·ipad
犀利豆4 小时前
#AI in Harness(一)
java·ai编程
极***涯4 小时前
深度拆解:多源BFS路径重构难题——跳出单层遍历误区,搞定高阶最短路径全量解
java·重构·宽度优先
YuK.W5 小时前
Leetcode100: 70.爬楼梯、118.杨辉三角、198.打家劫舍
java·算法·leetcode
这不小天嘛5 小时前
JAVA八股——redis篇
java·开发语言·redis