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

目录

    • 一、前言
    • 二、环境准备
      • Maven 依赖
    • 三、核心能力总览
    • 四、Shp → GeoJSON
      • 核心步骤
      • 关键代码
      • 注意事项
    • 五、GeoJSON → Shapefile(重点)
      • 坑位一:Schema 写死导致属性丢失
      • 坑位二:未显式绑定 CRS 导致 QGIS 无法渲染
      • 坑位三:3D 坐标导致写入异常
      • 完整代码
      • 2D 降维工具方法
    • 六、Geometry 与 WKT 互转
      • Geometry → WKT
      • WKT → Geometry
    • 七、Shapefile → WKT 批量导出
    • 八、完整可运行代码汇总
    • 九、常见坑位与注意事项
    • 十、工程实践建议
    • 十一、小结

一、前言

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

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

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


二、环境准备

Maven 依赖

<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 → GeoJSONFeatureJSON + GeometryJSONWebGIS 前端渲染
GeoJSON → ShpFeatureJSON 读取 + ShapefileDataStore 写入数据入库 / 分发
Geometry ↔ WKTWKTWriter / WKTReader日志 / 接口 / SQL
Shp → WKTDataStore + WKTWriter数据导出 / 人工审查

四、Shp → GeoJSON

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

核心步骤

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

关键代码

privatestaticvoidshpToGeoJSON(StringshpPath,StringgeojsonPath)throwsException{Map<String,Serializable>params=newHashMap<>();params.put("url",newFile(shpPath).toURI().toURL());params.put("charset",StandardCharsets.UTF_8.name());DataStorestore=null;try{store=DataStoreFinder.getDataStore(params);StringtypeName=store.getTypeNames()[0];FeatureCollection<?,?>collection=store.getFeatureSource(typeName).getFeatures();try(FileOutputStreamfos=newFileOutputStream(geojsonPath);BufferedOutputStreambos=newBufferedOutputStream(fos)){FeatureJSONfjson=newFeatureJSON(newGeometryJSON(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 坐标。

完整代码

// ==================== GeoJSON → Shp ===================privatestaticvoidgeojsonToShp(StringgeojsonPath,StringshpPath)throwsException{try(InputStreamin=newFileInputStream(geojsonPath)){FeatureJSONfjson=newFeatureJSON(newGeometryJSON());FeatureCollection<?,?>collection=fjson.readFeatureCollection(in);Map<String,Serializable>params=newHashMap<>();params.put(ShapefileDataStoreFactory.URLP.key,newFile(shpPath).toURI().toURL());params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key,true);ShapefileDataStoreds=null;try{ShapefileDataStoreFactoryfactory=newShapefileDataStoreFactory();ds=(ShapefileDataStore)factory.createNewDataStore(params);ds.setCharset(StandardCharsets.UTF_8);// 取第一个要素,推断几何类型 + 原始属性结构SimpleFeaturefirstFeature=null;try(FeatureIterator<?>it=collection.features()){if(it.hasNext()){firstFeature=(SimpleFeature)it.next();}}if(firstFeature==null){thrownewRuntimeException("GeoJSON 中没有任何要素");}GeometryfirstGeom=(Geometry)firstFeature.getDefaultGeometry();Class<?>geomType=firstGeom.getClass();// Shapefile 只支持 2D,强制降维if(geomType.getSimpleName().contains("3D")||firstGeom.getCoordinate().getZ()!=Double.NaN){if(firstGeominstanceofPoint){geomType=Point.class;}elseif(firstGeominstanceofLineString){geomType=LineString.class;}elseif(firstGeominstanceofPolygon){geomType=Polygon.class;}}// 动态构建 Schema(继承 GeoJSON 的字段)SimpleFeatureTypeBuildertb=newSimpleFeatureTypeBuilder();tb.setName("layer");tb.setCRS(org.geotools.referencing.CRS.decode("EPSG:4326"));tb.add("the_geom",geomType);// 继承 GeoJSON 中的非几何属性字段SimpleFeatureTypeoriginalSchema=(SimpleFeatureType)collection.getSchema();for(inti=0;i<originalSchema.getAttributeCount();i++){org.opengis.feature.type.AttributeDescriptorad=originalSchema.getDescriptor(i);if(adinstanceoforg.opengis.feature.type.GeometryDescriptor){continue;// 跳过几何字段}StringattrName=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()){SimpleFeaturefrom=(SimpleFeature)it.next();SimpleFeatureto=writer.next();// 写入几何(强制 2D)Geometryg=to2D((Geometry)from.getDefaultGeometry());to.setAttribute("the_geom",g);// 按字段名拷贝属性(不再写死 name/value)for(inti=0;i<originalSchema.getAttributeCount();i++){org.opengis.feature.type.AttributeDescriptorad=originalSchema.getDescriptor(i);if(adinstanceoforg.opengis.feature.type.GeometryDescriptor){continue;}StringattrName=ad.getLocalName();to.setAttribute(attrName,from.getAttribute(attrName));}writer.write();}}}}finally{if(ds!=null){ds.dispose();}}}System.out.println(" GeoJSON → Shp 完成");}

2D 降维工具方法

privatestaticGeometryto2D(Geometrygeom){if(geom==null)returnnull;geom=geom.copy();geom.apply(newCoordinateSequenceFilter(){@Overridepublicvoidfilter(CoordinateSequenceseq,inti){// 强制丢弃 Z / Mseq.setOrdinate(i,CoordinateSequence.X,seq.getX(i));seq.setOrdinate(i,CoordinateSequence.Y,seq.getY(i));}@OverridepublicbooleanisDone(){returnfalse;}@OverridepublicbooleanisGeometryChanged(){returntrue;}});returngeom;}

六、Geometry 与 WKT 互转

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

Geometry → WKT

privatestaticvoidgeometryToWKT(){Pointpoint=GF.createPoint(newCoordinate(116.407,39.904));WKTWriterwriter=newWKTWriter();System.out.println(" Geometry → WKT:");System.out.println(writer.write(point));}

WKT → Geometry

privatestaticvoidwktToGeometry()throwsException{WKTReaderreader=newWKTReader(GF);Geometrygeom=reader.read("POINT (116.415 39.912)");System.out.println(" WKT → Geometry:");System.out.println(geom.getClass().getSimpleName()+" : "+geom);}

七、Shapefile → WKT 批量导出

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

privatestaticvoidshpToWKT(StringshpPath,StringwktPath)throwsException{Map<String,Serializable>params=newHashMap<>();params.put("url",newFile(shpPath).toURI().toURL());params.put("charset",StandardCharsets.UTF_8.name());DataStorestore=null;try(PrintWriterpw=newPrintWriter(newOutputStreamWriter(newFileOutputStream(wktPath),StandardCharsets.UTF_8))){store=DataStoreFinder.getDataStore(params);StringtypeName=store.getTypeNames()[0];FeatureCollection<?,?>collection=store.getFeatureSource(typeName).getFeatures();WKTWriterwktWriter=newWKTWriter();try(FeatureIterator<?>it=collection.features()){intidx=0;while(it.hasNext()){SimpleFeaturef=(SimpleFeature)it.next();Geometrygeom=(Geometry)f.getDefaultGeometry();pw.println("# Feature "+idx++);pw.println(wktWriter.write(geom));}}}finally{if(store!=null)store.dispose();}System.out.println(" Shp → WKT 完成");}

八、完整可运行代码汇总

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

importorg.geotools.data.DataStore;importorg.geotools.data.DataStoreFinder;importorg.geotools.data.FeatureWriter;importorg.geotools.data.Transaction;importorg.geotools.data.shapefile.ShapefileDataStore;importorg.geotools.data.shapefile.ShapefileDataStoreFactory;importorg.geotools.feature.FeatureCollection;importorg.geotools.feature.FeatureIterator;importorg.geotools.feature.simple.SimpleFeatureTypeBuilder;importorg.geotools.geojson.feature.FeatureJSON;importorg.geotools.geojson.geom.GeometryJSON;importorg.locationtech.jts.geom.*;importorg.locationtech.jts.io.WKTReader;importorg.locationtech.jts.io.WKTWriter;importorg.opengis.feature.simple.SimpleFeature;importorg.opengis.feature.simple.SimpleFeatureType;importjava.io.*;importjava.nio.charset.StandardCharsets;importjava.util.HashMap;importjava.util.Map;publicclassGeoToolsFormatConvertDemo{// 全局 GeometryFactory(线程安全,推荐)privatestaticfinalGeometryFactoryGF=newGeometryFactory();publicstaticvoidmain(String[]args)throwsException{StringinputShp="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 不是 AutoCloseableGeoTools 29.x 移除了该接口使用 try/finally + dispose()
GeoJSON 属性丢失Schema 写死为固定字段动态读取 GeoJSON 的 Schema 继承字段
QGIS 提示无几何未显式绑定 CRStb.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 过程中最常见的属性丢失和几何缺失问题。