JTS 几何对象与 WKT/WKB 互转详解:基于 JTS 实现地理空间数据的统一处理

前言

在 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它是不支持三维的