前言
在 GIS(地理信息系统)开发中,几何数据的表示和传输是常见的需求。JTS(Java Topology Suite)作为 Java 生态中最成熟的几何计算库,提供了强大的几何对象模型。而在实际业务中,我们经常需要在 WKT、WKB 和 Geometry 对象之间进行转换。
| 格式 | 特点 | 应用场景 |
|---|---|---|
| WKT | 人类可读的文本格式 | 调试、日志记录、数据库存储 |
| WKB | 紧凑的二进制格式 | 网络传输 |
| Geometry | Java 对象 | 程序直接操作、数据库存储 |
本文将封装一个工具类 JtsConvertUtil,介绍如何高效地实现三者之间的互相转换。
一、核心依赖
Maven依赖如下,用的是jts1.19版本
java
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
代码中使用的是 locationtech.jts 提供的核心类:
java
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.*;
| 类 | 作用 |
|---|---|
WKTReader / WKTWriter |
WKT 字符串 ↔ Geometry |
WKBReader / WKBWriter |
WKB 字节数组 ↔ Geometry |
ByteOrderValues |
定义字节序(大端/小端) |
二、工具类整体结构
JtsConvertUtil 采用 静态方法 + 单例 Reader/Writer 的设计,避免重复创建对象,提升性能:
java
public class JtsConvertUtil {
// 定义所需变量,常量
private static final WKTReader WKT_READER = new WKTReader();
private static final WKTWriter WKT_WRITER = new WKTWriter();
private static final WKBReader WKB_READER = new WKBReader();
private static final int BIG_ENDIAN = ByteOrderValues.BIG_ENDIAN;
private static final int LITTLE_ENDIAN = ByteOrderValues.LITTLE_ENDIAN;
// 各个转换方法......
}
设计要点 :所有 Reader/Writer 声明为
static final,线程安全且无需重复实例化。
三、WKT ↔ Geometry 互转
3.1 WKT → Geometry
java
public static Geometry wktToGeometry(String wkt) {
if (wkt == null || wkt.isBlank()) {
return null;
}
try {
return WKT_READER.read(wkt);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid WKT: " + wkt, e);
}
}
这里直接用WKTReader
3.2 Geometry → WKT
java
public static String geometryToWKT(Geometry geometry) {
if (geometry == null || geometry.isEmpty()) {
return null;
}
return WKT_WRITER.write(geometry);
}
这里直接用WKTWriter就可以了,
WKTWriter构造函数其实还有个outputDimension参数,看了官方源码,
outputDimension参数可以是2,3,4,分别对于二维,带Z维度,带M维度,默认是2,支持的类型包括Point,LinearRing,LineString,Polygon,MultiPoint,MultiLineString,MultiPolygon,GeometryCollection
四、WKB ↔ Geometry 互转
4.1 WKB → Geometry
java
public static Geometry wkbToGeometry(byte[] wkb) {
if (wkb == null || wkb.length == 0) {
return null;
}
try {
return WKB_READER.read(wkb);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid WKB", e);
}
}
4.2 Geometry → WKB
java
public static byte[] geometryToWKB(Geometry geometry) {
if (geometry == null || geometry.isEmpty()) {
return null;
}
// 这里用BIG_ENDIAN,具体使用根据实际选择,也可以把byteOrder作为参数传进去
return new WKBWriter(2, BIG_ENDIAN).write(geometry);
}
| 字节序 | 常量 | 常用场景 |
|---|---|---|
| 大端 | BIG_ENDIAN |
PostGIS 推荐格式 |
| 小端 | LITTLE_ENDIAN |
MySQL / GeoPackage |
参数说明 :参数
2表示输出 WKB 2D 坐标。什么是大端序和小端序?
- 大端序(Big Endian) :数据的高位字节存储在内存的低地址端,符合人类读写习惯。例如 32 位整数
0x12345678在内存中存储为12 34 56 78,高位在前。网络传输普遍采用大端序(网络字节序),PostGIS 默认使用此格式。 - 小端序(Little Endian) :数据低位字节在前,
0x12345678存储为78 56 34 12。x86/x64 CPU 使用小端序,内存操作效率更高。MySQL Geometry 和 GeoPackage 标准均采用小端序。
WKB 格式通过首字节标识字节序:00 表示大端,01 表示小端。开发时根据目标数据库选择对应字节序即可。
这个版本的WKBWriter有这些方法:
1:public WKBWriter(int outputDimension, int byteOrder)
2:public WKBWriter(int outputDimension, boolean includeSRID)
3:public WKBWriter(int outputDimension, int byteOrder, boolean includeSRID)
这里的outputDimension代表维度,只能是2或者3,即二维或者三维(带Z维度),默认是2;
includeSRID代表是否包含srid
看了下WKBWriter源码,官方的默认值(即直接创建WKBWriter不指定任何参数)是这样的,二维,大端,不含srid,如下:
java
public WKBWriter() { this(2, ByteOrderValues.BIG_ENDIAN); }
public WKBWriter(int outputDimension, int byteOrder) {
this(outputDimension, byteOrder, false);
}
五、WKT ↔ WKB 直接转换 & HEX 辅助
这里jts并没有直接转的方法,需要先wkt转geometry,然后geometry转wkb
java
// WKT → WKB
public static byte[] wktToWKB(String wkt) {
Geometry g = wktToGeometry(wkt);
if (g == null) return null;
return geometryToWKB(g);
}
// WKB → WKT
public static String wkbToWKT(byte[] wkb) {
Geometry g = wkbToGeometry(wkb);
if (g == null) return null;
return geometryToWKT(g);
}
数据库中 WKB 通常以十六进制字符串存储,工具类提供了 HEX 编码/解码:
java
// WKB 字节数组 → 十六进制字符串
public static String wkbToHex(byte[] wkb) {
StringBuilder sb = new StringBuilder(wkb.length * 2);
for (byte b : wkb) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
// 十六进制字符串 → WKB 字节数组
public static byte[] hexToWKB(String hex) {
byte[] wkb = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
wkb[i / 2] = (byte) Integer.parseInt(
hex.substring(i, i + 2), 16);
}
return wkb;
}
六、测试
java
public static void main(String[] args) {
System.out.println("===== JtsConvertUtil Self Test =====\n");
String wkt = "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))";
Geometry g = wktToGeometry(wkt);
System.out.println("WKT → Geometry: " + g);
String wktBack = geometryToWKT(g);
System.out.println("Geometry → WKT: " + wktBack);
byte[] wkb = geometryToWKB(g);
System.out.println("Geometry → WKB length: " + wkb.length);
System.out.println("WKB HEX: " + wkbToHex(wkb));
Geometry g2 = wkbToGeometry(wkb);
System.out.println("WKB → Geometry: " + g2);
String wktFromWkb = wkbToWKT(wkb);
System.out.println("WKB → WKT: " + wktFromWkb);
byte[] wkb2 = hexToWKB(wkbToHex(wkb));
System.out.println("HEX → WKB → Geometry: " + wkbToGeometry(wkb2));
byte[] wkb3 = wktToWKB(wktFromWkb);
System.out.println("WKT -> WKB length" + wkb3.length);
System.out.println("WKB HEX" + wkbToHex(wkb3));
System.out.println("\n===== Test Finished =====");
}

七、转换关系全景图
以下是 WKT、WKB、Geometry 三者之间的完整转换关系:
#mermaid-svg-WhxzLsDtuYa46pcr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WhxzLsDtuYa46pcr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WhxzLsDtuYa46pcr .error-icon{fill:#552222;}#mermaid-svg-WhxzLsDtuYa46pcr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WhxzLsDtuYa46pcr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WhxzLsDtuYa46pcr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WhxzLsDtuYa46pcr .marker.cross{stroke:#333333;}#mermaid-svg-WhxzLsDtuYa46pcr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WhxzLsDtuYa46pcr p{margin:0;}#mermaid-svg-WhxzLsDtuYa46pcr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WhxzLsDtuYa46pcr .cluster-label text{fill:#333;}#mermaid-svg-WhxzLsDtuYa46pcr .cluster-label span{color:#333;}#mermaid-svg-WhxzLsDtuYa46pcr .cluster-label span p{background-color:transparent;}#mermaid-svg-WhxzLsDtuYa46pcr .label text,#mermaid-svg-WhxzLsDtuYa46pcr span{fill:#333;color:#333;}#mermaid-svg-WhxzLsDtuYa46pcr .node rect,#mermaid-svg-WhxzLsDtuYa46pcr .node circle,#mermaid-svg-WhxzLsDtuYa46pcr .node ellipse,#mermaid-svg-WhxzLsDtuYa46pcr .node polygon,#mermaid-svg-WhxzLsDtuYa46pcr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WhxzLsDtuYa46pcr .rough-node .label text,#mermaid-svg-WhxzLsDtuYa46pcr .node .label text,#mermaid-svg-WhxzLsDtuYa46pcr .image-shape .label,#mermaid-svg-WhxzLsDtuYa46pcr .icon-shape .label{text-anchor:middle;}#mermaid-svg-WhxzLsDtuYa46pcr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WhxzLsDtuYa46pcr .rough-node .label,#mermaid-svg-WhxzLsDtuYa46pcr .node .label,#mermaid-svg-WhxzLsDtuYa46pcr .image-shape .label,#mermaid-svg-WhxzLsDtuYa46pcr .icon-shape .label{text-align:center;}#mermaid-svg-WhxzLsDtuYa46pcr .node.clickable{cursor:pointer;}#mermaid-svg-WhxzLsDtuYa46pcr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WhxzLsDtuYa46pcr .arrowheadPath{fill:#333333;}#mermaid-svg-WhxzLsDtuYa46pcr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WhxzLsDtuYa46pcr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WhxzLsDtuYa46pcr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WhxzLsDtuYa46pcr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WhxzLsDtuYa46pcr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WhxzLsDtuYa46pcr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WhxzLsDtuYa46pcr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WhxzLsDtuYa46pcr .cluster text{fill:#333;}#mermaid-svg-WhxzLsDtuYa46pcr .cluster span{color:#333;}#mermaid-svg-WhxzLsDtuYa46pcr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WhxzLsDtuYa46pcr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WhxzLsDtuYa46pcr rect.text{fill:none;stroke-width:0;}#mermaid-svg-WhxzLsDtuYa46pcr .icon-shape,#mermaid-svg-WhxzLsDtuYa46pcr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WhxzLsDtuYa46pcr .icon-shape p,#mermaid-svg-WhxzLsDtuYa46pcr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WhxzLsDtuYa46pcr .icon-shape .label rect,#mermaid-svg-WhxzLsDtuYa46pcr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WhxzLsDtuYa46pcr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WhxzLsDtuYa46pcr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WhxzLsDtuYa46pcr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WKTReader
WKTWriter
WKBReader
WKBWriter
wktToWKB()
wkbToWKT()
hexToWKB()
wkbToHex()
WKT String
Geometry Object
WKB byte\[\]
HEX String(数据库存储)
总结
封装了 JTS 中三种几何表示形式的互转逻辑。实际开发中,几何数据的数据库字段类型可以是longtext,也可以是Geometry(这个Java那里要根据数据库不同自定义下TypeHandler来支持读写,会麻烦一点,这个后面可能会单独出篇文章),要注意下Mysql的Geometry它是不支持三维的