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 → GeoJSON | FeatureJSON + GeometryJSON | WebGIS 前端渲染 |
| GeoJSON → Shp | FeatureJSON 读取 + ShapefileDataStore 写入 | 数据入库 / 分发 |
| Geometry ↔ WKT | WKTWriter / WKTReader | 日志 / 接口 / SQL |
| Shp → WKT | DataStore + WKTWriter | 数据导出 / 人工审查 |
四、Shp → GeoJSON
这是最常见的转换场景,将 Shapefile 转换为 GeoJSON,便于 WebGIS 前端渲染。
核心步骤
DataStoreFinder读取 Shapefile- 获取
FeatureCollection 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 不是 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 |
十、工程实践建议
- 数据导入时动态推断几何类型,避免写死
- 始终显式绑定 CRS,确保生成的文件可被 QGIS / ArcGIS 正常识别
- GeoJSON → Shapefile 时动态继承原始属性字段,不要硬编码字段名
- 全局复用
GeometryFactory实例,避免重复创建 - 写入 Shapefile 后及时
dispose(),释放文件锁 - 使用
GeometryJSON(7)控制精度,平衡文件大小与精度
十一、小结
本文介绍了 GeoTools 中四种核心空间数据格式转换场景:Shapefile ↔ GeoJSON、GeoJSON → Shapefile、Geometry ↔ WKT以及Shapefile → WKT。通过动态 Schema 构建、显式 CRS 绑定和 2D 降维等技术,解决了 GeoJSON 转 Shapefile 过程中最常见的属性丢失和几何缺失问题。
