---
url: /java/gdal-java.md
---
# GDAL Java 绑定使用指南

## 简介

GDAL (Geospatial Data Abstraction Library) 是一个开源的地理空间数据转换库。Java 绑定允许在 Java 应用程序中使用 GDAL 的功能。

## 安装步骤

### 1. 系统要求

* Java Development Kit (JDK) 8 或更高版本
* Maven 或 Gradle 构建工具
* 原生 GDAL 库

### 2. 安装原生 GDAL 库

#### Windows

```bash
# 使用 OSGeo4W 安装
# 下载并运行 OSGeo4W 安装程序
# 选择安装 GDAL 组件
```

#### Linux (Ubuntu/Debian)

```bash
sudo apt-get update
sudo apt-get install gdal-bin libgdal-dev
```

#### macOS

```bash
brew install gdal
```

### 3. 配置环境变量

#### Windows

```batch
set GDAL_HOME=C:\OSGeo4W
set PATH=%PATH%;%GDAL_HOME%\bin
set JAVA_LIBRARY_PATH=%GDAL_HOME%\bin
```

#### Linux/macOS

```bash
export GDAL_HOME=/usr
export LD_LIBRARY_PATH=$GDAL_HOME/lib:$LD_LIBRARY_PATH
export JAVA_LIBRARY_PATH=$GDAL_HOME/lib
```

### 4. Maven 依赖

```xml
<dependencies>
    <!-- GDAL 官方 Java 绑定（纯 Java jar，不含原生库） -->
    <dependency>
        <groupId>org.gdal</groupId>
        <artifactId>gdal</artifactId>
        <version>3.11.0</version>
    </dependency>

    <!-- gdal-utils：官方提供的配套 jar（随 org.gdal:gdal 一同发布），
         包含若干辅助工具类。按需引入，版本与 gdal 保持一致。 -->
    <dependency>
        <groupId>org.gdal</groupId>
        <artifactId>gdal-utils</artifactId>
        <version>3.11.0</version>
    </dependency>
</dependencies>
```

**注意**：

* `org.gdal:gdal` 只是 **JNI 桥接的 Java 类**（SWIG 生成），jar 里**不包含** `gdalall_gdal.dll` / `libgdal.so` 等原生库，必须单独安装（见上一步），且**大版本号要和原生库一致**（jar 是 3.11.x 就配 GDAL 3.11.x 的原生库）。
* 另一个常见选择是 GeoSolutions 的 **jaiext-gdal**（`it.geosolutions.jaiext:jaiext-gdal`），它把 GDAL 包装成 JAI `RenderedOp`，适合做影像处理管线；本笔记以官方绑定为准。

### 5. Gradle 依赖

```groovy
dependencies {
    implementation 'org.gdal:gdal:3.11.0'
}
```

### 6. 原生库加载方式

Java 绑定通过 JNI 调用原生 GDAL，有三种常见加载方式：

1. **`-Djava.library.path`**（最常用）：把 GDAL 的 lib/bin 目录加进去，见下文"常见问题"。
2. **JNA/JNI 自动定位**：部分发行版（如 `gdal-utils` 附带工具类）支持从 classpath 解压 dll/so 后 `System.load()`。
3. **打包进 fat-jar**：不推荐；dll 需要落盘才能被 JVM 加载，通常用 maven-dependency-plugin 在启动前解到临时目录。

验证安装是否成功：

```java
gdal.AllRegister();
System.out.println(gdal.VersionInfo("VERSION_NUM"));   // 例如 3110000 = 3.11.0
```

## 基本用法

### 初始化 GDAL

```java
import org.gdal.gdal.gdal;
import org.gdal.ogr.ogr;

public class GDALBootstrap {

    public static void main(String[] args) {
        // 注册全部栅格驱动（gdal.AllRegister 在较新版本中为推荐入口；
        // 老版本用 gdal.RegisterAll()）
        gdal.AllRegister();
        // 注册全部矢量（OGR）驱动
        ogr.RegisterAll();

        System.out.println("GDAL Version: " + gdal.VersionInfo("VERSION_NUM"));
    }
}
```

> **约定**：下面所有示例都假定已完成上述初始化。每个类里写一次即可（例如放在 `main` 或 Spring 的 `@PostConstruct` 里）。

### 读取栅格数据

```java
import org.gdal.gdal.Band;
import org.gdal.gdal.Dataset;
import org.gdal.gdal.gdal;
import org.gdal.gdalconst.gdalconstConstants;
import org.gdal.osr.SpatialReference;

public class ReadRaster {

    public static void readRaster(String filePath) {
        // GA_ReadOnly = 0；也可省略第二个参数（默认只读）
        try (Dataset dataset = gdal.OpenEx(filePath,
                gdalconstConstants.OF_RASTER | gdalconstConstants.OF_VERBOSE_ERROR)) {
            if (dataset == null) {
                System.err.println("无法打开文件: " + filePath);
                return;
            }

            int xSize = dataset.getRasterXSize();
            int ySize = dataset.getRasterYSize();
            int bandCount = dataset.getRasterCount();
            System.out.println("尺寸: " + xSize + " x " + ySize);
            System.out.println("波段数: " + bandCount);

            // 地理变换：[左上角X, 像素宽, 行旋转, 左上角Y, 列旋转, 像素高(负值)]
            double[] gt = dataset.GetGeoTransform();
            System.out.printf("范围: (%.6f, %.6f) ~ (%.6f, %.6f)%n",
                    gt[0], gt[3] + ySize * gt[5],
                    gt[0] + xSize * gt[1], gt[3]);

            // 空间参考（WKT / EPSG）
            SpatialReference srs = dataset.GetSpatialRef();
            if (srs != null) {
                int epsg = -1;
                srs.AutoIdentifyEPSG();
                epsg = srs.GetAuthorityCode(null);
                System.out.println("坐标系: EPSG:" + epsg + " / " + srs.ExportToWkt());
                srs.delete();
            }

            // 读取第 1 波段（注意：buffer 类型必须与波段数据类型一致，
            // Float32 波段要用 float[]，Byte 波段用 byte[]，混用会报错）
            Band band = dataset.GetRasterBand(1);
            System.out.println("数据类型: " + band.GetRasterDataType()
                    + ", NoData=" + band.GetNoDataValue(true));

            float[] data = new float[xSize * ySize];
            band.ReadRaster(0, 0, xSize, ySize, data);

            // 统计信息（false=不强制重算，true=覆盖已有缓存）
            double[] stats = band.GetStatistics(false, true);
            System.out.printf("min=%.2f max=%.2f mean=%.2f std=%.2f%n",
                    stats[0], stats[1], stats[2], stats[3]);
        }
        // try-with-resources：Dataset 实现了 AutoCloseable，close() 内部调用 delete()
    }
}
```

**要点**：

* `gdal.Open` vs `gdal.OpenEx`：`OpenEx` 支持 flags（如 `OF_VECTOR`、`OF_RASTER`、`OF_READONLY`），还能直接打开带子数据集的格式（如 `file.tif/subdataset=1`）。
* `ReadRaster` 一次性读整幅大图容易 OOM，生产环境按块读（见"性能优化"）。
* 所有返回的 SWIG 包装对象（`Dataset`、`Band`、`SpatialReference`、`Feature`…）都持有原生内存，用完必须 `delete()`（或依赖 try-with-resources）。

### 写入栅格数据

```java
import org.gdal.gdal.Band;
import org.gdal.gdal.Dataset;
import org.gdal.gdal.Driver;
import org.gdal.gdal.gdal;
import org.gdal.gdalconst.gdalconstConstants;
import org.gdal.osr.SpatialReference;

public class WriteRaster {

    public static void writeRaster(String outputPath, int width, int height) {
        try (Driver driver = gdal.GetDriverByName("GTiff")) {
            // 创建选项：分块 + LZW 压缩（对 Web 服务读取很关键）
            String[] createOpts = { "TILED=YES", "BLOCKXSIZE=256", "BLOCKYSIZE=256",
                                    "COMPRESS=LZW" };
            try (Dataset dataset = driver.Create(outputPath, width, height, 1,
                    gdalconstConstants.GDT_Float32, createOpts)) {

                // 地理变换：[originX, pixelWidth, 0, originY, 0, -pixelHeight]
                // 注意第 5 个值（像素高）必须为负，表示 y 轴向下递减
                double pixelSize = 1.0;
                dataset.SetGeoTransform(new double[] { 0, pixelSize, 0,
                        height * pixelSize, 0, -pixelSize });

                // 坐标系：EPSG:4326
                SpatialReference srs = new SpatialReference();
                srs.importFromEPSG(4326);
                dataset.SetProjection(srs.ExportToWkt());
                srs.delete();

                // 写入数据
                Band band = dataset.GetRasterBand(1);
                band.SetNoDataValue(-9999.0);
                float[] data = new float[width * height];
                for (int i = 0; i < data.length; i++) data[i] = i % 1000 / 10f;
                band.WriteRaster(0, 0, width, height, data);

                // 刷新缓存到磁盘（close 也会做，但显式调用可提前落盘、便于出错定位）
                dataset.FlushCache();
            }
        }
    }
}
```

**要点**：

* `SetGeoTransform` 的六个参数含义是 `[c0, c1, c2, c3, c4, c5]`，对应
  `x = c0 + col*c1 + row*c2`、`y = c3 + col*c4 + row*c5`。常规北向上影像就是
  `[minX, 分辨率, 0, maxY, 0, -分辨率]`。
* `driver.Create(...)` 的最后一个参数 `GDT_*` 决定波段数据类型；多波段传 band 数即可。
* 大文件加 `BIGTIFF=IF_SAFER`；发布到地图服务建议 `TILED=YES` + 压缩 + 金字塔（见下文）。

### 矢量数据操作

```java
import org.gdal.ogr.DataSource;
import org.gdal.ogr.Feature;
import org.gdal.ogr.Geometry;
import org.gdal.ogr.Layer;
import org.gdal.ogr.ogr;

public class ReadVector {

    public static void readShapefile(String filePath) {
        try (DataSource dataSource = ogr.OpenEx(filePath, 0)) {
            if (dataSource == null) {
                System.err.println("无法打开文件: " + filePath);
                return;
            }

            int layerCount = dataSource.GetLayerCount();
            for (int i = 0; i < layerCount; i++) {
                Layer layer = dataSource.GetLayer(i);
                System.out.println("图层: " + layer.GetName()
                        + ", 几何类型: " + layer.GetGeomType());

                layer.ResetReading();
                Feature feature;
                while ((feature = layer.GetNextFeature()) != null) {
                    Geometry geom = feature.GetGeometryRef();
                    if (geom != null) {
                        System.out.println("  FID=" + feature.GetFID()
                                + " " + geom.GetGeometryName());
                        geom.delete();   // GetGeometryRef 返回的是引用，需手动释放
                    }
                    // 读属性：按字段名取（注意区分大小写）
                    String name = feature.GetFieldAsString("name");
                    double val = feature.GetFieldAsDouble("value");
                    feature.delete();
                }
                layer.delete();
            }
        }
    }
}
```

### 创建矢量数据

```java
import org.gdal.ogr.DataSource;
import org.gdal.ogr.Driver;
import org.gdal.ogr.FieldDefn;
import org.gdal.ogr.Feature;
import org.gdal.ogr.FeatureDefn;
import org.gdal.ogr.Geometry;
import org.gdal.ogr.Layer;
import org.gdal.ogr.ogr;
import org.gdal.osr.SpatialReference;

public class WriteVector {

    public static void createShapefile(String outputPath) throws Exception {
        try (Driver driver = ogr.GetDriverByName("ESRI Shapefile")) {
            try (DataSource ds = driver.CreateDataSource(outputPath)) {
                SpatialReference srs = new SpatialReference();
                srs.importFromEPSG(4326);

                // wkbPoint = 1（org.gdal.ogr.ogr.wkbPoint）
                Layer layer = ds.CreateLayer("points", srs, ogr.wkbPoint);
                FieldDefn field = new FieldDefn("name", ogr.OFTString);
                field.SetWidth(64);
                layer.CreateField(field);
                field.delete();

                FeatureDefn defn = layer.GetLayerDefn();
                try (Feature feat = new Feature(defn)) {
                    feat.SetField("name", "Test Point");
                    Geometry point = new Geometry(ogr.wkbPoint);
                    point.AddPoint_2D(116.4, 39.9);
                    feat.SetGeometry(point);
                    layer.CreateFeature(feat);
                }
                layer.SyncToDisk();
                srs.delete();
            }
        }
    }
}
```

**要点**：Shapefile 编码默认 ANSI，中文建议用 **GeoPackage (GPKG)** 或 **GeoJSON** 驱动替代；`CreateLayer` 的第三个参数是 OGR wkb 几何类型常量。

### 坐标转换

```java
import org.gdal.osr.CoordinateTransformation;
import org.gdal.osr.SpatialReference;

public class CoordinateTransform {

    /** 批量点转换：一次建转换器，循环内只调 TransformPoint（推荐做法） */
    public static double[][] transformBatch(double[][] points, int srcEpsg, int dstEpsg) {
        SpatialReference src = new SpatialReference();
        src.importFromEPSG(srcEpsg);
        SpatialReference dst = new SpatialReference();
        dst.importFromEPSG(dstEpsg);

        try (CoordinateTransformation ct = new CoordinateTransformation(src, dst)) {
            double[][] out = new double[points.length][2];
            for (int i = 0; i < points.length; i++) {
                // TransformPoint 会原地修改传入数组，返回 [x, y, z]
                double[] p = { points[i][0], points[i][1], 0 };
                ct.TransformPoint(p);
                out[i][0] = p[0];
                out[i][1] = p[1];
            }
            return out;
        } finally {
            src.delete();
            dst.delete();
        }
    }

    public static void main(String[] args) {
        // WGS84 -> Web Mercator
        double[][] res = transformBatch(new double[][] { { 116.4, 39.9 } }, 4326, 3857);
        System.out.printf("WebMercator: (%.2f, %.2f)%n", res[0][0], res[0][1]);
    }
}
```

**要点**：

* `CoordinateTransformation` 构建有成本（要加载网格/参数），**不要在循环里反复 new**；跨多个 SRS 对时可用 `Map<Pair, CoordinateTransformation>` 缓存。
* 常用 EPSG：`4326` WGS84 经纬度、`3857` Web Mercator（Google/OSM 瓦片）、`4490` CGCS2000、`32650` UTM 50N；国内常见地方坐标系用 EPSG:4610~4619 系列（CGCS2000 / 北京54 / 西安80 的 3 度带与高斯投影，具体带号按所在经度查）。
* 更高精度场景可给 `SpatialReference` 设置七参数（`SetTOWGS84`）或自定义 gridshift。

## 高级操作

### 重投影栅格（gdal.Warp）

Java 绑定把 C++ 的 warp 能力暴露为 `gdal` 类的静态方法，**选项用 `String[]` 传，键名与 `gdalwarp` CLI 相同**（不带前导 `-`）：

```java
import org.gdal.gdal.Dataset;
import org.gdal.gdal.gdal;

public class Reproject {

    public static void reproject(String in, String out, int dstEpsg) {
        String[] options = {
            "dstSRS=EPSG:" + dstEpsg,   // 目标坐标系
            "r=BILINEAR",               // 重采样: NEAREST / BILINEAR / CUBIC / LANCZOS
            "format=GTiff",
            "co=TILED=YES", "co=COMPRESS=LZW"
        };
        // 一步写出新文件（对应 CLI: gdalwarp -t_srs EPSG:xxxx in.tif out.tif）
        try (Dataset src = gdal.Open(in)) {
            try (Dataset dst = gdal.Warp(out, src, options)) {
                // dst 非空即成功；可继续读它的元数据
            }
        }
    }
}
```

* 等价 CLI 对照：`gdalwarp -t_srs EPSG:3857 -r bilinear in.tif out.tif`。
* 常用选项：`te=`（目标 extent，四个值按 minx maxy 顺序给的是 `left bottom right top`，注意与 Python `gdal.WarpOptions` 的 `outputBounds` 语义一致）、`tr=`（目标分辨率）、`srcNodata=` / `dstNodata=`、`cutline=`、`cropToCutline=YES`、`multi=CREATE_ONLY`（多线程分块写）。
* 需要精细控制输出尺寸/对齐时，可用 `gdal.AutoCreateWarpedDataset` 先算出目标数据集再 `WarpCreateCopy`（签名随版本略有差异，以所用 jar 的 javadoc 为准）。

### 按矢量边界裁剪

```java
String[] options = {
    "cutlineDSName=" + clipShp,          // 边界 shp / gpkg / geojson
    "cropToCutline=YES",                  // 输出范围收缩到 cutline 的最小外包框
    "srcNodata=0",
    "dstAlpha=YES"                        // 加 alpha 波段做透明遮罩（可选）
};
try (Dataset src = gdal.Open(rasterPath)) {
    try (Dataset dst = gdal.Warp("clipped.tif", src, options)) { }
}
```

### 合并多幅栅格（镶嵌）

**推荐做法：先 `gdalbuildvrt` 生成 VRT，再 `gdalwarp`/`Translate` 落地** —— VRT 只是描述性 XML，不复制像素，几百上千幅图也不怕。Java 里通常分两步：

```java
import java.io.*;
import org.gdal.gdal.Dataset;
import org.gdal.gdal.gdal;

public class Mosaic {

    /** 调用 gdalbuildvrt CLI 生成 .vrt（需 gdal 在 PATH 中） */
    public static void buildVrt(String vrtPath, String... rasters) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
                "gdalbuildvrt", "-q", "-resolution", "average", vrtPath);
        for (String r : rasters) pb.command().add(r);
        pb.redirectErrorStream(true);
        Process p = pb.start();
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(p.getInputStream()))) {
            String line; while ((line = br.readLine()) != null) System.err.println(line);
        }
        if (p.waitFor() != 0) throw new IOException("gdalbuildvrt failed");
    }

    /** 把 VRT 翻译成实体 GeoTIFF */
    public static void vrtToTiff(String vrtPath, String outPath) {
        try (Dataset vrt = gdal.Open(vrtPath)) {
            try (Dataset out = gdal.Translate(outPath, vrt, new String[] {
                    "format=GTiff", "co=TILED=YES", "co=COMPRESS=LZW" })) { }
        }
    }
}
```

> Java 绑定也有 `gdal.BuildVRT(...)` 静态方法（返回 `VRTDataset`），但重载较多、随版本变化，写死签名容易踩坑；生产代码里用上面的 CLI + `Translate` 组合最稳。纯 Java 不想依赖 CLI 时，也可直接用下面的 Warp 合并。

**不用 VRT 的替代方案：`gdal.Warp` 直接吃多源**（对应 Python 里把 list 传给 `gdal.Warp`）：

```java
String[] files = { "tile1.tif", "tile2.tif", "tile3.tif" };
Dataset[] srcs = new Dataset[files.length];
for (int i = 0; i < files.length; i++) srcs[i] = gdal.Open(files[i]);
try {
    try (Dataset dst = gdal.Warp("mosaic.tif", srcs, new String[] {
            "format=GTiff", "r=NEAREST", "co=TILED=YES", "co=COMPRESS=LZW" })) { }
} finally {
    for (Dataset d : srcs) if (d != null) d.delete();
}
```

### 生成金字塔（overviews）

```java
import org.gdal.gdal.Dataset;
import org.gdal.gdal.gdal;
import org.gdal.gdalconst.gdalconstConstants;

try (Dataset ds = gdal.OpenEx("large_image.tif", gdalconstConstants.GA_Update)) {
    // NEAREST 适合分类/离散数据；BILINEAR/AVERAGE 适合连续影像
    int ok = ds.BuildOverviews("NEAREST", new int[] { 2, 4, 8, 16, 32 });
    System.out.println(ok == 0 ? "金字塔生成成功" : "金字塔生成失败");
}
```

发布给 Web 地图服务（MapServer/GeoServer/自研切片）前，标准动作是：**TILED=YES + COMPRESS + BuildOverviews**。

### 格式转换（gdal.Translate）

```java
try (Dataset src = gdal.Open("input.jp2")) {
    String[] options = {
        "format=GTiff",
        "co=COMPRESS=LZW", "co=TILED=YES",
        "a_nodata=-9999",           // 设置 NoData
        "scale_min=0", "scale_max=255"  // 可选：拉伸取值范围
    };
    try (Dataset dst = gdal.Translate("output.tif", src, options)) { }
}
```

批量转换时外层套 `Files.list()` / glob 即可；大文件建议 `multi=CREATE_ONLY` 并行分块。

## 常见问题解决

### 1. 找不到原生库

**错误**: `java.lang.UnsatisfiedLinkError`

**解决方案**:

* 确保 GDAL 原生库已正确安装
* 设置 `java.library.path` JVM 参数:
  ```bash
  java -Djava.library.path=/path/to/gdal/lib -jar your-app.jar
  ```

### 2. 驱动程序未注册

**错误**: 无法打开特定格式的文件

**解决方案**:

```java
// 在使用前调用
gdal.AllRegister();
ogr.RegisterAll();
```

### 3. 内存管理

GDAL Java 绑定使用原生资源，需要手动释放:

```java
Dataset ds = gdal.Open("file.tif", gdalconstConstants.GA_ReadOnly);
try {
    // 使用数据集
} finally {
    ds.delete();  // 重要：释放原生资源
}
```

## 性能优化建议

### 1. 分块读取大栅格（防 OOM）

```java
import org.gdal.gdal.Band;
import org.gdal.gdal.Dataset;
import org.gdal.gdal.gdal;

public static void readInChunks(String path, int chunk, java.util.function.BiConsumer<float[], int[]> consumer) {
    try (Dataset ds = gdal.Open(path)) {
        Band band = ds.GetRasterBand(1);
        int w = ds.getRasterXSize(), h = ds.getRasterYSize();
        float[] buf = new float[chunk * chunk];   // 复用同一块 buffer
        for (int y0 = 0; y0 < h; y0 += chunk) {
            int ys = Math.min(chunk, h - y0);
            for (int x0 = 0; x0 < w; x0 += chunk) {
                int xs = Math.min(chunk, w - x0);
                band.ReadRaster(x0, y0, xs, ys, buf);
                consumer.accept(buf, new int[] { x0, y0, xs, ys });
            }
        }
    }
}
```

* `chunk` 取驱动块大小的整数倍（GTiff 默认 256×256）命中率最高。
* 多波段数据按"整行所有波段一起读"组织循环，减少 IO 次数。

### 2. 多线程并行处理瓦片

GDAL 的 `Dataset`/`Band` 对象**不是线程安全的**，正确姿势是"每线程独立 Open 自己的子窗口"：

```java
ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<?>> futures = new ArrayList<>();
for (int tile : tiles) {
    futures.add(pool.submit(() -> {
        try (Dataset ds = gdal.Open(basePath)) {          // 每线程独立打开
            processTile(ds, tile);
        }
    }));
}
futures.forEach(f -> f.get());
pool.shutdown();
```

配合 warp 时的 `multi=CREATE_ONLY`、写 GTiff 时的 `NUM_THREADS=ALL_CPUS` 创建选项，可进一步压榨单任务内部并行度。

### 3. 其他要点

1. **重用对象**：`CoordinateTransformation`、`Driver` 等构建成本高，缓存复用，别在循环里 new。
2. **创建选项**：发布用影像固定加 `TILED=YES` + `COMPRESS=LZW/DEFLATE` + 金字塔；大数据量加 `BIGTIFF=IF_SAFER`。
3. **内存映射**：只读场景可用 `MEM:` 驱动把小数据集整体载入内存，后续读写全走 RAM。
4. **日志**：`gdal.SetConfigOption("CPL_LOG", "/tmp/gdal.log")` 或 `"CPL_DEBUG"` 排查驱动问题。

## 参考资源

* [GDAL 官方文档](https://gdal.org/)
* [GDAL Java 绑定源码（swig/java）](https://github.com/OSGeo/gdal/tree/master/swig/java) —— Java API 以随 jar 发布的 javadoc 为准
* [Maven Central: org.gdal](https://central.sonatype.com/search?smartspace=org.gdal)
* [GitHub 仓库](https://github.com/OSGeo/gdal)

# GDAL Java 绑定（Java Bindings）全面技术总结

> 基于 OSGeo/gdal 官方仓库、Context7 文档与公开技术资料整理，涵盖架构、安装、API、示例代码与最佳实践。

***

## 目录

1. [概述](#1-概述)
2. [核心概念与架构](#2-核心概念与架构)
3. [环境搭建与依赖管理](#3-环境搭建与依赖管理)
4. [栅格数据 API（Raster）](#4-栅格数据-apiraster)
5. [矢量数据 API（OGR Vector）](#5-矢量数据-apiogr-vector)
6. [空间参考与坐标变换（OSR）](#6-空间参考与坐标变换osr)
7. [常用工具类与回调机制](#7-常用工具类与回调机制)
8. [完整示例程序](#8-完整示例程序)
9. [性能优化与线程安全](#9-性能优化与线程安全)
10. [常见问题与故障排查](#10-常见问题与故障排查)
11. [版本演进与兼容性](#11-版本演进与兼容性)
12. [参考资料](#12-参考资料)

***

## 1. 概述

### 1.1 什么是 GDAL

**GDAL（Geospatial Data Abstraction Library）** 是一个在 X/MIT 许可协议下的开源地理空间数据转换库，最初作为 C/C++ 库开发，提供对 **200+ 种栅格格式** 和 **100+ 种矢量格式** 的统一读写接口。其核心组件包括：

| 组件 | 说明 |
|------|------|
| **GDAL（gcore）** | 栅格数据抽象层，处理影像的读写、波段操作、重采样等 |
| **OGR** | 矢量数据抽象层（Simple Features 模型），处理点/线/面要素及属性表 |
| **OSR / PROJ** | 空间参考系统（Spatial Reference System）与坐标投影变换引擎 |
| **CPL（port）** | 底层平台支持库，封装文件 I/O、字符串、网络请求等跨平台功能 |
| **alg** | 算法库，包含重采样、直方图、地形分析（坡度/坡向/山体阴影）等 |
| **drivers（frmts）** | 各格式的驱动实现，每个格式一个驱动插件 |

### 1.2 什么是 Java 绑定

GDAL 官方通过 **SWIG**（Simplified Wrapper and Interface Generator）自动生成 Java 绑定，生成产物为：

* **`gdal.jar`** — 纯 Java 类库，包含 `org.gdal.*` 包下的所有包装类
* **`libgdalalljni.so` / `gdalalljni.dll` / `libgdalalljni.dylib`** — JNI 本地库，桥接 Java 与底层 C++ libgdal

Java 绑定中的类与方法命名基本与 C++ 原类一一对应，因此熟悉 GDAL C++ API 的开发者可以平滑迁移。

> **重要提示**：由于 Java GC 运行在主线程之外的独立线程中，GDAL 必须配置为多线程支持编译，否则即使单线程使用也可能出现崩溃。这是使用 Java 绑定时最容易被忽略的前提条件。

### 1.3 典型应用场景

* 遥感影像批量读取、格式转换、裁剪、重投影
* 多源矢量数据（Shapefile / GeoJSON / GeoPackage / GDB）导入导出
* GIS 服务端（GeoServer、ArcGIS Enterprise 扩展）的空间数据处理后端
* 地理空间 ETL 管道中的统一数据访问层
* 科学计算（NetCDF、HDF5、GRIB）与 GIS 数据的互操作

***

## 2. 核心概念与架构

### 2.1 对象层次结构

```
┌─────────────────────────────────────────────────────────────┐
│                        栅格侧（GDAL gcore）                   │
│                                                             │
│  gdal (静态入口)                                             │
│   ├── AllRegister()          注册所有驱动                    │
│   ├── Open(filename, flags)  打开数据集                      │
│   └── GetDriverByName(name)  按名称获取驱动                  │
│                                                             │
│  Driver                                                            │
│   ├── Create(...)/CreateCopy(...)                                │
│   └── OpenDataset(...)                                           │
│                                                             │
│  Dataset                                                           │
│   ├── getRasterXSize() / getRasterYSize()                       │
│   ├── getRasterCount()                                           │
│   ├── GetProjectionRef() → String (WKT)                         │
│   ├── GetGeoTransform(double[6])                                 │
│   ├── GetRasterBand(bandIndex) → Band   (1-based!)              │
│   ├── SetGeoTransform(...)                                       │
│   └── FlushCache()                                               │
│                                                             │
│  Band                                                                │
│   ├── ReadRaster(...) / WriteRaster(...)                          │
│   ├── ReadBlock(xOff, yOff, dataBuffer)                           │
│   ├── GetStatistics(...)                                          │
│   ├── ComputeRasterMinMax(...)                                    │
│   ├── GetMaskBand() / GetMaskFlags()                              │
│   ├── GetOverview(i) → Band                                       │
│   └── SetNoDataValue(dfVal)                                       │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                      矢量侧（OGR）                            │
│                                                             │
│  ogr (静态入口)                                              │
│   └── RegisterAll()         注册所有矢量驱动                 │
│                                                             │
│  Driver (org.gdal.ogr.Driver)                                │
│   └── Open(name, bUpdate) → DataSource                        │
│   └── CreateDataSource(...)                                   │
│                                                             │
│  DataSource                                                    │
│   ├── GetLayer(count) → Layer                                  │
│   ├── ExecuteSQL(stmt, spatialFilter, dialect) → Layer         │
│   └── ReleaseDS()                                              │
│                                                             │
│  Layer                                                         │
│   ├── GetNextFeature() → Feature                               │
│   ├── ResetReading()                                           │
│   ├── CreateFeature(feature)                                   │
│   ├── GetGeomFieldDefn(i)                                      │
│   └── SetSpatialFilter(geometry)                               │
│                                                             │
│  Feature                                                       │
│   ├── GetFieldDefn(i) → FieldDefn                              │
│   ├── GetFieldAsString/AsDouble/AsInteger(i)                   │
│   ├── SetField(name, value)                                    │
│   ├── GetGeometryRef() → Geometry                              │
│   └── SetGeometry(geom)                                        │
│                                                             │
│  Geometry                                                      │
│   ├── Clone()                                                  │
│   ├── Transform(coordTransformation)                           │
│   ├── ExportToWkt(String[])                                    │
│   ├── getEnvelope(double[4])                                   │
│   └── Buffer(distance, outGeom)                                │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                  空间参考侧（OSR）                             │
│                                                             │
│  SpatialReference                                                │
│   ├── new SpatialReference(wkt)                                  │
│   ├── ImportFromEPSG(int code)                                    │
│   ├── ImportFromWkt(String[])                                     │
│   ├── ExportToWkt(String[])                                       │
│   ├── ExportToProj4(String[])                                     │
│   ├── SetWellKnownGeogCS("WGS84")                                 │
│   ├── SetUTM(zone, north)                                         │
│   └── IsSame(otherSRS)                                            │
│                                                             │
│  CoordinateTransformation                                        │
│   ├── CreateCoordinateTransformation(srcSRS, dstSRS)               │
│   ├── TransformPoint(double[] xyz)  // in-place, xyz[0..2]        │
│   └── TransformPoints(double[] pts, int n)                        │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 关键设计要点

| 要点 | 说明 |
|------|------|
| **带号从 1 开始** | `GetRasterBand(1)` 是第一个波段，不是 0，这是 GDAL 历史遗留约定，极易出错 |
| **显式资源释放** | SWIG 生成的对象调用 `.delete()` 释放原生内存；也可依赖 GC 最终回收，但高并发下建议显式释放 |
| **错误码模式** | 多数方法返回 `int` 错误码（`CE_None = 0` 表示成功），而非抛异常；可调用 `gdal.UseExceptions()` 切换为异常模式 |
| **数组传递** | 许多 C 风格 API 在 Java 中以 `double[]`/`int[]`/`String[]` 作为输出参数（in-out 语义），需预分配长度 |
| **Vector 容器** | SWIG 将 C++ `std::vector<T>` 映射为 `java.util.Vector<T>`（非 ArrayList），遍历时用 `Enumeration` |

***

## 3. 环境搭建与依赖管理

### 3.1 Maven 方式（推荐）

GDAL Java 绑定发布在 **Maven Central**，groupId 为 `org.gdal`：

```xml
<dependency>
    <groupId>org.gdal</groupId>
    <artifactId>gdal</artifactId>
    <version>3.8.0</version>
</dependency>
```

> **注意**：Maven Central 上的 `gdal.jar` **不包含本地 .so/.dll**。JAR 只含 Java 类，本地库仍需单独部署到系统的动态库搜索路径。

### 3.2 本地库部署

#### Linux

```bash
# 方式一：设置环境变量（当前会话有效）
export LD_LIBRARY_PATH=/opt/gdal/lib:$LD_LIBRARY_PATH

# 方式二：永久生效（写入 /etc/ld.so.conf.d/gdal.conf）
echo "/opt/gdal/lib" > /etc/ld.so.conf.d/gdal.conf
sudo ldconfig
```

#### macOS

```bash
export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH
# Homebrew 安装的 GDAL 若未启用 Java 绑定，需源码编译并指定 --with-java
```

#### Windows

```bat
set PATH=C:\gdal\bin;C:\gdal\java;%PATH%
:: gdalalljni.dll 放在 bin 目录或 java 目录均可
```

**Windows 预编译包推荐来源**：

* [Tamas Szekeres' gisinternals.com SDK](http://www.gisinternals.com/sdk) — 包含 Win32/Win64 完整二进制 + Java 绑定
* OSGeo4W 安装包（选择 `gdal` + `gdal-java` 组件）

### 3.3 从源码构建

```bash
# 前置：JDK 7+、SWIG、Ant（必须在 PATH 中）、CMake
cmake -DBUILD_JAVA_BINDINGS=ON \
      -DCMAKE_INSTALL_PREFIX=/opt/gdal \
      ..

make -j$(nproc)
sudo make install
# 产物：
#   /opt/gdal/share/java/gdal.jar
#   /opt/gdal/lib/libgdalalljni.so  (GDAL >= 3.8)
```

关键 CMake 选项：

| 选项 | 默认值 | 说明 |
|------|--------|------|
| `BUILD_JAVA_BINDINGS` | `ON`（检测到 JDK 时） | 是否构建 Java 绑定 |
| `GDAL_JAVA_INSTALL_DIR` | `${DATADIR}/java` | gdal.jar 安装目录 |
| `GDAL_JAVA_JNI_INSTALL_DIR` | （3.8+） | JNI 本地库安装目录 |

### 3.4 验证安装

```bash
# Linux 快速验证（从源码构建目录运行）
export LD_LIBRARY_PATH=$PWD:$PWD/swig/java:$LD_LIBRARY_PATH
java -classpath $PWD/swig/java/gdal.jar:$PWD/swig/java/build/apps gdalinfo test.tif

# Windows
set PATH=%CD%;%CD%\swig\java;%PATH%
java -classpath %CD%\swig\java\gdal.jar;%CD%\swig\java\build\apps gdalinfo test.tif
```

或写一个最小 Java 测试类：

```java
import org.gdal.gdal.gdal;

public class TestGdal {
    public static void main(String[] args) {
        gdal.AllRegister();
        System.out.println("GDAL Version: " + gdal.VersionInfo(""));
        // 期望输出: GDAL 3.x.x, released YYYY/MM/DD
    }
}
```

***

## 4. 栅格数据 API（Raster）

### 4.1 初始化与打开数据集

```java
import org.gdal.gdal.gdal;
import org.gdal.gdal.Dataset;
import org.gdal.gdal.Band;
import org.gdal.gdalconst.gdalconstConstants;

// ① 注册所有驱动（每次 JVM 启动只需调用一次）
gdal.AllRegister();

// 可选：开启异常模式（替代错误码检查）
gdal.UseExceptions();

// ② 打开数据集
Dataset ds = gdal.Open("/path/to/image.tif", gdalconstConstants.GA_ReadOnly);
if (ds == null) {
    throw new RuntimeException("无法打开数据集: " + gdal.GetLastErrorMsg());
}
```

打开标志（`AccessFlag`）：

| 常量 | 值 | 含义 |
|------|----|------|
| `GA_ReadOnly` | 0 | 只读 |
| `GA_Update` | 1 | 读写 |
| `GA_Extended` | 2 | 扩展模式（某些驱动需要） |

### 4.2 读取基本信息

```java
// 尺寸
int width  = ds.getRasterXSize();
int height = ds.getRasterYSize();
int bands  = ds.getRasterCount();

// 投影（WKT 字符串）
String wkt = ds.GetProjectionRef();

// 仿射变换参数（GeoTransform）
// [originX, pixelWidth, rowRotation, originY, colRotation, pixelHeight]
double[] gt = new double[6];
ds.GetGeoTransform(gt);

// 像素中心的世界坐标
double worldX = gt[0] + gt[1] * col + gt[2] * row;
double worldY = gt[3] + gt[4] * col + gt[5] * row;

// 元数据
java.util.Vector<String> metadata = ds.GetMetadata_List("");
```

### 4.3 波段读取（核心）

```java
Band band = ds.GetRasterBand(1);  // 注意：从 1 开始！

// --- 方式 A：整幅读取（适合小图或内存充足场景）---
int xSize = ds.getRasterXSize();
int ySize = ds.getRasterYSize();
byte[] buf = new byte[xSize * ySize];
band.ReadRaster(0, 0, xSize, ySize, buf, xSize, ySize,
                band.getDataType(), 0, 0);

// --- 方式 B：分块读取（推荐用于大图，避免 OOM）---
int[] blockSize = new int[2];
band.GetBlockSize(blockSize);
int blockW = blockSize[0], blockH = blockSize[1];

for (int by = 0; by < ySize; by += blockH) {
    for (int bx = 0; bx < xSize; bx += blockW) {
        int rw = Math.min(blockW, xSize - bx);
        int rh = Math.min(blockH, ySize - by);
        byte[] blockBuf = new byte[rw * rh];
        band.ReadRaster(bx, by, rw, rh, blockBuf, rw, rh,
                        band.getDataType(), 0, 0);
        // 处理 blockBuf ...
    }
}

// --- 方式 C：ReadBlock（直接按物理块读取，最快）---
byte[] blockData = new byte[blockW * blockH];
band.ReadBlock(0, 0, blockData);  // xOff, yOff 以块为单位
```

### 4.4 波段写入与创建新数据集

```java
// 创建新的 GeoTIFF
org.gdal.gdal.Driver driver = gdal.GetDriverByName("GTiff");
Dataset newDs = driver.Create("output.tif", 512, 512, 3,
                              gdalconstConstants.GDT_Byte, null);

// 设置地理变换与投影
newDs.SetGeoTransform(new double[]{444720, 30, 0, 3751320, 0, -30});
newDs.SetProjection("PROJCS[\"NAD27 / UTM zone 11N\", ...]");  // WKT

// 写入波段数据
Band outBand = newDs.GetRasterBand(1);
outBand.SetDescription("Red");
byte[] rasterData = generateRasterData(512, 512);  // 自行填充
outBand.WriteRaster(0, 0, 512, 512, rasterData, 512, 512,
                    gdalconstConstants.GDT_Byte, 0, 0);

// 设置 NoData
outBand.SetNoDataValue(0.0);

// 刷新缓存并关闭
newDs.FlushCache();
newDs.delete();
```

数据类型常量对照：

| 常量 | C 类型 | 字节数 | 说明 |
|------|--------|--------|------|
| `GDT_Byte` | uint8 | 1 | 无符号 8 位 |
| `GDT_UInt16` | uint16 | 2 | 无符号 16 位 |
| `GDT_Int16` | int16 | 2 | 有符号 16 位 |
| `GDT_UInt32` | uint32 | 4 | 无符号 32 位 |
| `GDT_Int32` | int32 | 4 | 有符号 32 位 |
| `GDT_Float32` | float | 4 | 单精度浮点 |
| `GDT_Float64` | double | 8 | 双精度浮点 |
| `GDT_CInt16` | c-int16 | 4 | 复数（实部 int16） |
| `GDT_CFloat32` | c-float32 | 8 | 复数（实部 float32） |

### 4.5 统计信息与质量评估

```java
// 精确统计（全图扫描，较慢）
double[] min = {0}, max = {0}, mean = {0}, stdDev = {0};
int err = band.GetStatistics(false, false, min, max, mean, stdDev);
if (err == gdalconstConstants.CE_None) {
    System.out.printf("Min=%.2f Max=%.2f Mean=%.2f StdDev=%.2f%n",
            min[0], max[0], mean[0], stdDev[0]);
}

// 近似统计（采样，快得多）
band.GetStatistics(true, true, min, max, mean, stdDev);

// 计算精确 Min/Max（不依赖文件头信息）
double[] computedMM = new double[2];
band.ComputeRasterMinMax(computedMM, 0);

// 直方图
double[] histMin = {min[0]}, histMax = {max[0]};
int[][] histogram = new int[1][];
band.GetDefaultHistogram(histMin[0], histMax[0], histogram, true,
                         new org.gdal.gdal.TermProgressCallback());

// Checksum（用于校验）
int checksum = band.Checksum();
```

### 4.6 掩膜带（Alpha / NoData Mask）

```java
int maskFlags = band.GetMaskFlags();
// GMF_NODATA     = 1  存在 NoData 值定义
// GMF_ALL_VALID  = 2  所有像素均有效
// GMF_PER_DATASET= 4  掩膜适用于整个数据集
// GMF_ALPHA      = 8  Alpha 通道

if ((maskFlags & (gdalconstConstants.GMF_NODATA | gdalconstConstants.GMF_ALL_VALID)) == 0) {
    Band maskBand = band.GetMaskBand();
    // maskBand 中 0 = 无效，255 = 有效
    byte[] maskData = new byte[width * height];
    maskBand.ReadRaster(0, 0, width, height, maskData, width, height,
                        gdalconstConstants.GDT_Byte, 0, 0);
}
```

### 4.7 概览（Overviews / Pyramids）

```java
int overviewCount = band.GetOverviewCount();
for (int i = 0; i < overviewCount; i++) {
    Band ov = band.GetOverview(i);
    System.out.println("Overview " + i + ": " + ov.getXSize() + "x" + ov.getYSize());
}

// 生成概览（加速大尺度显示）
// 在 Python 中对应 gdaladdo；Java 中可通过 Dataset.BuildOverviews 调用
ds.BuildOverviews("Average", 1, new int[]{2, 4, 8, 16}, 0, null, null);
```

### 4.8 颜色表与调色板

```java
org.gdal.gdal.ColorTable ct = band.GetRasterColorTable();
if (ct != null) {
    int count = ct.GetCount();
    for (int i = 0; i < Math.min(count, 10); i++) {
        org.gdal.gdal.ColorEntry entry = ct.GetColorEntry(i);
        System.out.printf("%d: R=%d G=%d B=%d A=%d%n",
                i, entry.getC1(), entry.getC2(), entry.getC3(), entry.getC4());
    }
}
```

***

## 5. 矢量数据 API（OGR Vector）

### 5.1 初始化与打开数据源

```java
import org.gdal.ogr.ogr;
import org.gdal.ogr.DataSource;
import org.gdal.ogr.Layer;
import org.gdal.ogr.Feature;
import org.gdal.ogr.Geometry;
import org.gdal.ogr.FieldDefn;
import org.gdal.ogr.FeatureDefn;

// 注册所有矢量驱动
ogr.RegisterAll();

// 打开数据源（支持 shp、geojson、gpkg、sqlite、kml 等）
DataSource ds = ogr.GetDriverByName("ESRI Shapefile").Open("data.shp", 0);
// 或直接让 GDAL 自动探测：
DataSource dsAuto = ogr.Open("data.shp", 0);
```

### 5.2 遍历图层与要素

```java
int layerCount = ds.GetLayerCount();
for (int li = 0; li < layerCount; li++) {
    Layer layer = ds.GetLayer(li);
    String layerName = layer.GetName();
    System.out.println("Layer: " + layerName);

    // 几何类型
    System.out.println("Geom Type: " +
        org.gdal.ogr.ogr.GetGeometryTypeName(layer.GetGeomType()));

    // 字段定义
    FeatureDefn defn = layer.GetLayerDefn();
    int fieldCount = defn.GetFieldCount();
    for (int fi = 0; fi < fieldCount; fi++) {
        FieldDefn fdefn = defn.GetFieldDefn(fi);
        System.out.printf("  Field[%d]: %s (%s)%n", fi,
            fdefn.GetName(),
            org.gdal.ogr.ogr.GetFieldTypeName(fdefn.GetType()));
    }

    // 遍历要素
    layer.ResetReading();
    Feature feature;
    while ((feature = layer.GetNextFeature()) != null) {
        // 属性
        String name = feature.GetFieldAsString("NAME");
        double value = feature.GetFieldAsDouble("VALUE");

        // 几何
        Geometry geom = feature.GetGeometryRef();
        if (geom != null) {
            String[] wkt = new String[1];
            geom.ExportToWkt(wkt);
            System.out.println("  " + name + " -> " + wkt[0]);
        }
        feature.delete();  // 释放原生内存
    }
}
ds.delete();
```

### 5.3 空间过滤与 SQL 查询

```java
// 空间过滤：只读取与给定几何相交的要素
Layer layer = ds.GetLayer(0);

Geometry filterGeom = new Geometry();
filterGeom.fromWkt("POLYGON((116.0 39.0, 117.0 39.0, 117.0 40.0, 116.0 40.0, 116.0 39.0))");
layer.SetSpatialFilter(filterGeom);

long count = layer.GetFeatureCount();
System.out.println("Matching features: " + count);

// 属性 SQL 查询（OGR SQL 方言）
Layer sqlLayer = ds.ExecuteSQL(
    "SELECT * FROM my_layer WHERE population > 1000000 ORDER BY name",
    null,   // spatialFilter
    "OGRSQL" // dialect
);
// ... 处理结果 ...
ds.ReleaseDS(sqlLayer);  // 必须释放 SQL 结果图层
```

支持的 SQL 方言：`OGRSQL`（通用）、`SQLITE`、`GEOS`、`MYSQL`、`POSTGRES`、`MSSQL` 等，取决于底层驱动能力。

### 5.4 创建矢量数据

```java
// 创建 GeoJSON 文件
org.gdal.ogr.Driver geoJsonDriver = ogr.GetDriverByName("GeoJSON");
DataSource outDs = geoJsonDriver.CreateDataSource("output.geojson", null);

// 创建图层
SpatialReference srs = new SpatialReference();
srs.ImportFromEPSG(4326);  // WGS84

Layer outLayer = outDs.CreateLayer("points", srs,
    org.gdal.ogr.wkbConstants.wkbPoint, null);

// 添加字段
outLayer.CreateField(new FieldDefn("name", org.gdal.ogr.ogr.OFTString, 64, 0));
outLayer.CreateField(new FieldDefn("elevation", org.gdal.ogr.ogr.OFTReal, 0, 2));

// 创建要素
Feature feat = new Feature(outLayer.GetLayerDefn());
feat.SetField("name", "北京");
feat.SetField("elevation", 43.5);

Geometry point = new Geometry();
point.AssignPoint(116.4074, 39.9042, 0);
feat.SetGeometry(point);

outLayer.CreateFeature(feat);
feat.delete();
point.delete();

outDs.delete();
```

### 5.5 几何操作

```java
Geometry geom = feature.GetGeometryRef();

// 边界框
double[] env = new double[4];
geom.getEnvelope(env);  // [minX, maxX, minY, maxY]

// 缓冲区
Geometry bufferGeom = new Geometry();
geom.Buffer(0.001, bufferGeom, 0);  // ~100m @ equator

// 面积 / 长度
double area = geom.Area();       // 仅对 Polygon 有意义
double length = geom.Length();   // 仅对 LineString 有意义

// 克隆（深拷贝）
Geometry copy = geom.Clone();

// 几何求交（需要 GEOS 支持）
Geometry other = ...;
Geometry intersection = new Geometry();
// 通过 ogr 的几何运算 API 或 OGR SQL 的 INTERSECTS 谓词
```

### 5.6 常见矢量格式驱动名

| 驱动短名 | 格式 |
|----------|------|
| `ESRI Shapefile` | ESRI Shapefile (.shp) |
| `GeoJSON` | GeoJSON |
| `GPKG` | GeoPackage |
| `SQLite` | SQLite (+ SpatiaLite) |
| `Memory` | 内存临时层 |
| `KML` / `LIBKML` | KML / KMZ |
| `CSV` | CSV（无几何或 WKT 列） |
| `FlatGeobuf` | FlatGeobuf |
| `GML` | GML 2/3 |
| `DXF` | AutoCAD DXF |
| `MapInfo File` | MapInfo TAB |
| `PostgreSQL` | PostGIS |
| `MySQL` | MySQL Spatial |

***

## 6. 空间参考与坐标变换（OSR）

### 6.1 SpatialReference 基本操作

```java
import org.gdal.osr.SpatialReference;

// 从 EPSG 代码创建
SpatialReference srs = new SpatialReference();
srs.ImportFromEPSG(4326);  // WGS84 经纬度

// 从 WKT 字符串导入
SpatialReference srsFromWkt = new SpatialReference();
srsFromWkt.ImportFromWkt(new String[]{wktString});

// 从 Proj4 字符串导入
srs.ImportFromProj4("+proj=utm +zone=50 +datum=WGS84 +units=m +no_defs");

// 导出为各种格式
String[] wktOut = new String[1];
srs.ExportToWkt(wktOut);

String[] proj4Out = new String[1];
srs.ExportToProj4(proj4Out);

String[] prettyWktOut = new String[1];
srs.ExportToPrettyWkt(prettyWktOut, 0);

// 判断两个 SRS 是否相同
boolean same = srs.IsSame(otherSrs);

// 获取地理坐标系（如投影坐标系转回 WGS84）
SpatialReference geogCS = srs.CloneGeogCS();
```

### 6.2 坐标变换

```java
import org.gdal.osr.CoordinateTransformation;

// 源：EPSG:4326 (WGS84)
SpatialReference srcSrs = new SpatialReference();
srcSrs.ImportFromEPSG(4326);

// 目标：EPSG:3857 (Web Mercator)
SpatialReference dstSrs = new SpatialReference();
dstSrs.ImportFromEPSG(3857);

// 创建变换器（开销较大，建议复用）
CoordinateTransformation ct =
    CoordinateTransformation.CreateCoordinateTransformation(srcSrs, dstSrs);

// 单点变换（in-place，xyz[0]=x, xyz[1]=y, xyz[2]=z）
double[] point = {116.4074, 39.9042, 0};
ct.TransformPoint(point);
System.out.printf("Mercator: %.2f, %.2f%n", point[0], point[1]);
// 输出: Mercator: 12958175.00, 4852833.00

// 批量变换（更高效）
double[] points = {
    116.4074, 39.9042, 0,   // 北京
    121.4737, 31.2304, 0,   // 上海
    113.2644, 23.1291, 0    // 广州
};
ct.TransformPoints(points, 3);

// 清理
ct.delete();
srcSrs.delete();
dstSrs.delete();
```

### 6.3 栅格重投影（配合算法库）

GDAL Java 绑定中重投影通常通过 `gdal.AutoCreateWarpedVRT` 或 `gdal.Warp` 完成：

```java
import org.gdal.gdal.gdal;

// 方式一：创建虚拟重投影 VRT（内存中，不落盘）
Dataset warpedVrt = gdal.AutoCreateWarpedVRT(
    srcDataset,           // 源数据集
    null,                 // srcGeoTransform（null 则用数据集自带的）
    srcSrsWkt,            // 源 SRS WKT
    dstSrsWkt,            // 目标 SRS WKT
    gdalconstConstants.GCubic,  // 重采样算法
    0,                    // 容差
    null                  // warp options
);
// 然后像普通 Dataset 一样读取 warpedVrt

// 方式二：直接 Warp 到文件
gdal.Warp("output_reprojected.tif", "input.tif",
    gdalconstConstants.GCubic,
    new String[]{"-t_srs", "EPSG:3857"});
```

重采样算法常量：

| 常量 | 说明 | 适用场景 |
|------|------|----------|
| `GNear` | 最近邻 | 分类数据、整数栅格 |
| `GBilinear` | 双线性 | 连续数据（默认推荐） |
| `GCubic` | 三次卷积 | 高质量连续数据 |
| `GCubicSpline` | 三次样条 | 平滑插值 |
| `GLanczos` | Lanczos | 最高质量（慢） |
| `GAverage` | 平均 | 降采样聚合 |
| `GRMS` | 均方根 | 降采样保留方差 |

***

## 7. 常用工具类与回调机制

### 7.1 进度回调

```java
import org.gdal.gdal.ProgressCallback;
import org.gdal.gdal.TermProgressCallback;

// 终端进度条（内置实现）
TermProgressCallback progress = new TermProgressCallback();

// 自定义进度回调
ProgressCallback myProgress = new ProgressCallback() {
    @Override
    public int run(double fraction, String message, void* data) {
        // fraction: 0.0 ~ 1.0
        System.out.printf("\rProgress: %.1f%% %s", fraction * 100, message);
        return 1;  // 返回 1 继续，0 取消
    }
};
```

### 7.2 全局配置项（Configuration Options）

```java
// 等价于设置环境变量，优先级高于实际环境变量
gdal.SetConfigOption("GDAL_CACHEMAX", "512");           // 缓存大小（MB）
gdal.SetConfigOption("GDAL_NUM_THREADS", "4");          // 线程数
gdal.SetConfigOption("CPL_VSIL_CURL_TIMEOUT", "30");    // HTTP 超时（秒）
gdal.SetConfigOption("OGR_SQLITE_SPATIALITE", "YES");   // 启用 SpatiaLite

// 读取当前值
String cacheMax = gdal.GetConfigOption("GDAL_CACHEMAX");

// 清除
gdal.SetConfigOption("GDAL_CACHEMAX", null);
```

常用配置项：

| 配置项 | 作用 |
|--------|------|
| `GDAL_CACHEMAX` | 内存缓存上限（MB），默认约 25% 可用 RAM |
| `GDAL_NUM_THREADS` | 并行读写的线程数 |
| `GDAL_USE_RRD` | 是否使用 RRD 概览（旧版 TIFF） |
| `CPL_DEBUG` | 设为 `ON` 开启调试日志 |
| `CPL_LOG` | 日志文件路径 |
| `OGR_GEOMETRY_TYPES` | 限制允许的几何类型 |
| `SHAPE_ENCODING` | Shapefile 编码（UTF-8 / GBK 等） |

### 7.3 日志与错误处理

```java
// 错误码常量
gdalconstConstants.CE_None      // 0 - 成功
gdalconstConstants.CPLError     // 1 - 一般错误
gdalconstConstants.CE_Failure   // 2 - 严重错误
gdalconstConstants.CE_Fatal     // 3 - 致命错误

// 获取最后错误
int lastErrNo = gdal.GetLastErrorNo();
String lastErrMsg = gdal.GetLastErrorMsg();

// 异常模式（推荐）
gdal.UseExceptions();
try {
    Dataset ds = gdal.Open("nonexistent.tif", 0);
} catch (Exception e) {
    System.err.println("打开失败: " + e.getMessage());
}

// 调试日志
gdal.SetConfigOption("CPL_DEBUG", "ON");
gdal.SetConfigOption("CPL_LOG", "/tmp/gdal_debug.log");
```

### 7.4 VSI 虚拟文件系统

GDAL 的 VSI（Virtual SI）层允许透明访问远程与压缩文件，在 Java 中直接使用路径前缀即可：

```java
// 普通文件
gdal.Open("/local/path/file.tif", 0);

// ZIP 内文件（无需解压）
gdal.Open("/vsizip/archive.zip!subdir/file.tif", 0);

// GZIP
gdal.Open("/vsigzip/data.tif.gz", 0);

// HTTP(S)
gdal.Open("/vsicurl/https://example.com/raster.tif", 0);

// S3（需要 AWS 凭证环境变量）
gdal.Open("/vsis3/bucket-name/path/file.tif", 0);

// 内存缓冲
Dataset memDs = gdal.GetDriverByName("MEM")
    .Create("", width, height, 1, gdalconstConstants.GDT_Byte, null);
```

***

## 8. 完整示例程序

### 8.1 栅格信息查看器（类似 gdalinfo）

```java
import org.gdal.gdal.*;
import org.gdal.gdalconst.gdalconstConstants;
import org.gdal.osr.SpatialReference;
import org.gdal.osr.CoordinateTransformation;
import java.util.Enumeration;
import java.util.Vector;

/**
 * 栅格信息查看器 —— Java 版 gdalinfo
 */
public class RasterInfoViewer {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java RasterInfoViewer <raster-file>");
            System.exit(1);
        }

        gdal.AllRegister();
        gdal.UseExceptions();

        try {
            Dataset ds = gdal.Open(args[0], gdalconstConstants.GA_ReadOnly);
            printDatasetInfo(ds);
            ds.delete();
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            System.exit(1);
        } finally {
            gdal.GDALDestroyDriverManager();
        }
    }

    private static void printDatasetInfo(Dataset ds) {
        Driver driver = ds.GetDriver();
        System.out.println("Driver: " + driver.getShortName()
                         + " / " + driver.getLongName());

        int w = ds.getRasterXSize();
        int h = ds.getRasterYSize();
        int b = ds.getRasterCount();
        System.out.printf("Size: %d x %d (bands: %d)%n", w, h, b);

        // 投影
        String projWkt = ds.GetProjectionRef();
        if (projWkt != null && !projWkt.isEmpty()) {
            SpatialReference srs = new SpatialReference(projWkt);
            String[] pretty = new String[1];
            srs.ExportToPrettyWkt(pretty, 0);
            System.out.println("Coordinate System:\n" + pretty[0]);
            srs.delete();
        }

        // GeoTransform
        double[] gt = new double[6];
        ds.GetGeoTransform(gt);
        if (gt[2] == 0 && gt[4] == 0) {
            System.out.printf("Origin: (%.6f, %.6f)%n", gt[0], gt[3]);
            System.out.printf("Pixel Size: (%.6f, %.6f)%n", gt[1], gt[5]);
        } else {
            System.out.println("GeoTransform:");
            System.out.printf("  %.6f, %.6f, %.6f%n", gt[0], gt[1], gt[2]);
            System.out.printf("  %.6f, %.6f, %.6f%n", gt[3], gt[4], gt[5]);
        }

        // 角点坐标（经纬度）
        printCorner(ds, "Upper Left ", 0, 0);
        printCorner(ds, "Lower Left ", 0, h);
        printCorner(ds, "Upper Right", w, 0);
        printCorner(ds, "Lower Right", w, h);
        printCorner(ds, "Center     ", w / 2.0, h / 2.0);

        // 波段详情
        for (int i = 0; i < b; i++) {
            Band band = ds.GetRasterBand(i + 1);
            int[] bs = new int[2];
            band.GetBlockSize(bs);

            Double[] min = {null}, max = {null};
            band.GetMinimum(min);
            band.GetMaximum(max);

            System.out.printf("Band %d:%n", i + 1);
            System.out.printf("  Block: %dx%d%n", bs[0], bs[1]);
            System.out.printf("  Type: %s%n", gdal.GetDataTypeName(band.getDataType()));
            System.out.printf("  ColorInterp: %s%n",
                gdal.GetColorInterpretationName(band.GetRasterColorInterpretation()));

            if (min[0] != null || max[0] != null) {
                System.out.printf("  Min: %s, Max: %s%n", min[0], max[0]);
            }

            Double[] noData = {null};
            band.GetNoDataValue(noData);
            if (noData[0] != null) {
                System.out.printf("  NoData: %s%n", noData[0]);
            }

            if (band.GetOverviewCount() > 0) {
                System.out.print("  Overviews: ");
                for (int oi = 0; oi < band.GetOverviewCount(); oi++) {
                    Band ov = band.GetOverview(oi);
                    if (oi > 0) System.out.print(", ");
                    System.out.print(ov.getXSize() + "x" + ov.getYSize());
                }
                System.out.println();
            }
        }
    }

    private static void printCorner(Dataset ds, String label, double px, double py) {
        double[] gt = new double[6];
        ds.GetGeoTransform(gt);
        double geoX = gt[0] + gt[1] * px + gt[2] * py;
        double geoY = gt[3] + gt[4] * px + gt[5] * py;

        System.out.print(label + "(" + geoX + ", " + geoY + ")");

        String projWkt = ds.GetProjectionRef();
        if (projWkt != null && !projWkt.isEmpty()) {
            SpatialReference srcSrs = new SpatialReference(projWkt);
            SpatialReference latLong = srcSrs.CloneGeogCS();
            if (latLong != null) {
                CoordinateTransformation ct =
                    CoordinateTransformation.CreateCoordinateTransformation(srcSrs, latLong);
                if (ct != null) {
                    double[] pt = {geoX, geoY, 0};
                    ct.TransformPoint(pt);
                    System.out.printf(" (%s, %s)",
                        gdal.DecToDMS(pt[0], "Long", 2),
                        gdal.DecToDMS(pt[1], "Lat", 2));
                    ct.delete();
                }
            }
            if (latLong != null) latLong.delete();
            srcSrs.delete();
        }
        System.out.println();
    }
}
```

### 8.2 矢量数据格式转换器（类似 ogr2ogr 简化版）

```java
import org.gdal.gdal.gdal;
import org.gdal.ogr.*;
import org.gdal.ogr.ogrConstants;
import org.gdal.osr.SpatialReference;
import org.gdal.osr.CoordinateTransformation;

/**
 * 简化版矢量格式转换器
 * 用法: java VectorConverter <src-file> <dst-file> [dst-srs-wkt-or-epsg]
 */
public class VectorConverter {

    public static void main(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: java VectorConverter <src> <dst> [dst-srs]");
            System.exit(1);
        }

        gdal.AllRegister();
        ogr.RegisterAll();
        gdal.UseExceptions();

        String srcFile = args[0];
        String dstFile = args[1];
        String dstSrsStr = args.length > 2 ? args[2] : null;

        try {
            // 打开源
            DataSource srcDs = ogr.Open(srcFile, 0);
            if (srcDs == null) {
                throw new RuntimeException("无法打开源文件: " + srcFile);
            }

            // 确定目标驱动
            String dstDriverName = guessDriver(dstFile);
            org.gdal.ogr.Driver dstDriver = ogr.GetDriverByName(dstDriverName);
            if (dstDriver == null) {
                throw new RuntimeException("未知的目标格式: " + dstDriverName);
            }

            // 创建目标数据源
            DataSource dstDs = dstDriver.CreateDataSource(dstFile, null);

            SpatialReference dstSrs = null;
            if (dstSrsStr != null) {
                dstSrs = new SpatialReference();
                if (dstSrsStr.matches("\\d+")) {
                    dstSrs.ImportFromEPSG(Integer.parseInt(dstSrsStr));
                } else {
                    dstSrs.ImportFromWkt(new String[]{dstSrsStr});
                }
            }

            long totalFeatures = 0;
            for (int li = 0; li < srcDs.GetLayerCount(); li++) {
                Layer srcLayer = srcDs.GetLayer(li);
                String layerName = srcLayer.GetName();

                // 创建对应目标图层
                SpatialReference layerSrs = srcLayer.GetSpatialRef();
                SpatialReference outSrs = dstSrs != null ? dstSrs : layerSrs;

                Layer dstLayer = dstDs.CreateLayer(
                    layerName, outSrs, srcLayer.GetGeomType(), null);

                // 复制字段
                FeatureDefn srcDefn = srcLayer.GetLayerDefn();
                for (int fi = 0; fi < srcDefn.GetFieldCount(); fi++) {
                    FieldDefn srcField = srcDefn.GetFieldDefn(fi);
                    dstLayer.CreateField(new FieldDefn(
                        srcField.GetName(),
                        srcField.GetType(),
                        srcField.GetWidth(),
                        srcField.GetPrecision()));
                }

                // 准备坐标变换
                CoordinateTransformation ct = null;
                if (dstSrs != null && layerSrs != null
                        && !layerSrs.IsSame(dstSrs)) {
                    ct = CoordinateTransformation.CreateCoordinateTransformation(
                        layerSrs, dstSrs);
                }

                // 逐要素转换
                srcLayer.ResetReading();
                Feature srcFeat;
                long count = 0;
                while ((srcFeat = srcLayer.GetNextFeature()) != null) {
                    Feature dstFeat = new Feature(dstLayer.GetLayerDefn());

                    // 复制属性
                    for (int fi = 0; fi < srcDefn.GetFieldCount(); fi++) {
                        String fieldName = srcDefn.GetFieldDefn(fi).GetName();
                        switch (srcDefn.GetFieldDefn(fi).GetType()) {
                            case ogr.OFTString:
                                dstFeat.SetField(fieldName,
                                    srcFeat.GetFieldAsString(fieldName));
                                break;
                            case ogr.OFTInteger:
                                dstFeat.SetField(fieldName,
                                    srcFeat.GetFieldAsInteger(fieldName));
                                break;
                            case ogr.OFTReal:
                                dstFeat.SetField(fieldName,
                                    srcFeat.GetFieldAsDouble(fieldName));
                                break;
                            default:
                                dstFeat.SetField(fieldName,
                                    srcFeat.GetFieldAsString(fieldName));
                        }
                    }

                    // 复制并变换几何
                    Geometry srcGeom = srcFeat.GetGeometryRef();
                    if (srcGeom != null) {
                        Geometry dstGeom = srcGeom.Clone();
                        if (ct != null) {
                            dstGeom.Transform(ct);
                        }
                        dstFeat.SetGeometryDirectly(dstGeom);
                    }

                    dstLayer.CreateFeature(dstFeat);
                    dstFeat.delete();
                    srcFeat.delete();
                    count++;
                }

                System.out.printf("Layer '%s': %d features converted%n",
                    layerName, count);
                totalFeatures += count;

                if (ct != null) ct.delete();
                if (layerSrs != null && dstSrs == null) layerSrs.delete();
            }

            System.out.println("Total: " + totalFeatures + " features");
            dstDs.delete();
            srcDs.delete();
            if (dstSrs != null) dstSrs.delete();

        } catch (Exception e) {
            System.err.println("Conversion failed: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static String guessDriver(String filename) {
        String lower = filename.toLowerCase();
        if (lower.endsWith(".shp")) return "ESRI Shapefile";
        if (lower.endsWith(".geojson") || lower.endsWith(".json")) return "GeoJSON";
        if (lower.endsWith(".gpkg")) return "GPKG";
        if (lower.endsWith(".kml")) return "KML";
        if (lower.endsWith(".kmz")) return "LIBKML";
        if (lower.endsWith(".csv")) return "CSV";
        if (lower.endsWith(".fgb")) return "FlatGeobuf";
        if (lower.endsWith(".tab")) return "MapInfo File";
        if (lower.endsWith(".dxf")) return "DXF";
        throw new RuntimeException("无法推断目标格式: " + filename);
    }
}
```

### 8.3 坐标批量转换工具

```java
import org.gdal.gdal.gdal;
import org.gdal.osr.SpatialReference;
import org.gdal.osr.CoordinateTransformation;

/**
 * 批量坐标转换：WGS84 → 指定目标坐标系
 */
public class BatchReproject {

    public static void main(String[] args) {
        gdal.AllRegister();

        // 源：WGS84
        SpatialReference srcSrs = new SpatialReference();
        srcSrs.ImportFromEPSG(4326);

        // 目标：CGCS2000 / 3-degree Gauss-Kruger CM 114E（示例）
        // 这里用 EPSG:4547 (CGCS2000 / 3-degree GK CM 114E)
        SpatialReference dstSrs = new SpatialReference();
        dstSrs.ImportFromEPSG(4547);

        CoordinateTransformation ct =
            CoordinateTransformation.CreateCoordinateTransformation(srcSrs, dstSrs);

        // 中国主要城市 WGS84 坐标
        String[][] cities = {
            {"北京", "116.4074", "39.9042"},
            {"上海", "121.4737", "31.2304"},
            {"广州", "113.2644", "23.1291"},
            {"成都", "104.0665", "30.5723"},
            {"乌鲁木齐", "87.6168", "43.8256"},
        };

        System.out.printf("%-10s %-12s %-12s %-15s %-15s%n",
            "城市", "Lon(WGS84)", "Lat(WGS84)", "X(目标)", "Y(目标)");
        System.out.println("-".repeat(64));

        for (String[] city : cities) {
            double[] pt = {
                Double.parseDouble(city[1]),
                Double.parseDouble(city[2]),
                0
            };
            int result = ct.TransformPoint(pt);
            if (result == 0) {
                System.out.printf("%-10s %-12.6f %-12.6f %-15.2f %-15.2f%n",
                    city[0], city[1], city[2], pt[0], pt[1]);
            } else {
                System.out.printf("%-10s FAILED%n", city[0]);
            }
        }

        ct.delete();
        srcSrs.delete();
        dstSrs.delete();
    }
}
```

### 8.4 栅格裁剪与子集读取

```java
import org.gdal.gdal.*;
import org.gdal.gdalconst.gdalconstConstants;

/**
 * 按地理范围裁剪栅格
 */
public class RasterClipper {

    public static void main(String[] args) throws Exception {
        gdal.AllRegister();
        gdal.UseExceptions();

        String srcFile = "large_raster.tif";
        String dstFile = "clipped_output.tif";

        // 裁剪范围（世界坐标）
        double clipMinX = 444720.0;
        double clipMinY = 3751000.0;
        double clipMaxX = 445500.0;
        double clipMaxY = 3751500.0;

        Dataset srcDs = gdal.Open(srcFile, gdalconstConstants.GA_ReadOnly);

        double[] gt = new double[6];
        srcDs.GetGeoTransform(gt);

        // 计算像素范围
        int xStart = (int) Math.floor((clipMinX - gt[0]) / gt[1]);
        int yStart = (int) Math.floor((gt[3] - clipMaxY) / gt[5]);  // 注意 Y 轴方向
        int xEnd   = (int) Math.ceil ((clipMaxX - gt[0]) / gt[1]);
        int yEnd   = (int) Math.ceil ((gt[3] - clipMinY) / gt[5]);

        // 边界保护
        xStart = Math.max(0, xStart);
        yStart = Math.max(0, yStart);
        xEnd = Math.min(srcDs.getRasterXSize(), xEnd);
        yEnd = Math.min(srcDs.getRasterYSize(), yEnd);

        int clipW = xEnd - xStart;
        int clipH = yEnd - yStart;
        int bandCount = srcDs.getRasterCount();

        System.out.printf("Clipping region: [%d,%d] - [%d,%d] (%dx%d pixels)%n",
            xStart, yStart, xEnd, yEnd, clipW, clipH);

        // 创建输出
        org.gdal.gdal.Driver tiffDriver = gdal.GetDriverByName("GTiff");
        Dataset dstDs = tiffDriver.Create(dstFile, clipW, clipH, bandCount,
            gdalconstConstants.GDT_Float32, null);

        // 设置地理参考（偏移裁剪原点）
        double[] dstGt = new double[6];
        dstGt[0] = gt[0] + gt[1] * xStart;
        dstGt[1] = gt[1];
        dstGt[2] = gt[2];
        dstGt[3] = gt[3] + gt[5] * yStart;
        dstGt[4] = gt[4];
        dstGt[5] = gt[5];
        dstDs.SetGeoTransform(dstGt);
        dstDs.SetProjection(srcDs.GetProjectionRef());

        // 逐波段复制数据
        for (int b = 1; b <= bandCount; b++) {
            Band srcBand = srcDs.GetRasterBand(b);
            Band dstBand = dstDs.GetRasterBand(b);

            float[] buf = new float[clipW * clipH];
            srcBand.ReadRaster(xStart, yStart, clipW, clipH,
                buf, clipW, clipH, gdalconstConstants.GDT_Float32, 0, 0);
            dstBand.WriteRaster(0, 0, clipW, clipH,
                buf, clipW, clipH, gdalconstConstants.GDT_Float32, 0, 0);

            Double[] noData = {null};
            srcBand.GetNoDataValue(noData);
            if (noData[0] != null) {
                dstBand.SetNoDataValue(noData[0]);
            }
        }

        dstDs.FlushCache();
        dstDs.delete();
        srcDs.delete();
        System.out.println("Done: " + dstFile);
    }
}
```

***

## 9. 性能优化与线程安全

### 9.1 内存管理

| 策略 | 说明 |
|------|------|
| **显式 `delete()`** | 对长时间持有的 Dataset/Band/Feature 对象，用完立即调用 `.delete()` 释放原生堆内存，不要依赖 GC |
| **分块读取** | 大图务必使用 `ReadBlock` 或按 `GetBlockSize` 分块 `ReadRaster`，避免一次性加载整个阵列导致 OOM |
| **调整缓存** | `gdal.SetConfigOption("GDAL_CACHEMAX", "1024")` 增大内存缓存（单位 MB） |
| **使用 MEM 驱动** | 中间结果放内存数据集，避免反复磁盘 I/O |

### 9.2 多线程注意事项

```java
// GDAL 本身是线程安全的（大部分 API），但以下对象不可跨线程共享：
// - Dataset 实例
// - Band 实例
// - Feature 实例
// - CoordinateTransformation 实例

// 正确做法：每个线程打开自己的 Dataset
class Worker implements Runnable {
    private final String filePath;
    Worker(String path) { this.filePath = path; }

    @Override
    public void run() {
        Dataset ds = gdal.Open(filePath, gdalconstConstants.GA_ReadOnly);
        // 在 ds 上操作...
        ds.delete();  // 在线程结束时释放
    }
}
```

> **关键**：Java GC 在独立线程中运行，而 GDAL 原生对象的生命周期由 C++ 侧管理。若 GDAL 未以多线程模式编译，GC 触发时的原生内存回收可能引发段错误。**生产环境务必确认 GDAL 是以 `-DWITH_THREADS=YES`（或 CMake 默认）编译的。**

### 9.3 I/O 优化

| 技巧 | 效果 |
|------|------|
| 读取前调用 `SetCacheFlusher` | 控制缓存刷新时机，减少不必要 flush |
| 使用 `/vsicurl/` 配合 HTTP Range 请求 | 云存储按需下载，无需全量拉取 |
| 对频繁访问的小区域建立 Overview | 大幅减少读取的数据量 |
| `GDAL_NUM_THREADS` 设为 CPU 核数 | 并行块级 I/O |
| 矢量数据用 `GetFeatureCount()` 预分配集合 | 避免 ArrayList 扩容 |

### 9.4 大数据集处理模式

```java
// 流式处理模式：边读边处理边丢弃，峰值内存恒定
Dataset ds = gdal.Open(hugeFile, gdalconstConstants.GA_ReadOnly);
int blockW, blockH;
int[] bs = new int[2];
ds.GetRasterBand(1).GetBlockSize(bs);
blockW = bs[0]; blockH = bs[1];

for (int by = 0; by < ds.getRasterYSize(); by += blockH) {
    for (int bx = 0; bx < ds.getRasterXSize(); bx += blockW) {
        int rw = Math.min(blockW, ds.getRasterXSize() - bx);
        int rh = Math.min(blockH, ds.getRasterYSize() - by);
        byte[] block = new byte[rw * rh];
        ds.GetRasterBand(1).ReadRaster(bx, by, rw, rh,
            block, rw, rh, gdalconstConstants.GDT_Byte, 0, 0);
        processBlock(block, bx, by, rw, rh);  // 处理后立即丢弃
    }
}
```

***

## 10. 常见问题与故障排查

### 10.1 `UnsatisfiedLinkError: no gdalalljni in java.library.path`

**原因**：JNI 本地库不在 JVM 搜索路径中。

**解决**：

```bash
# Linux
export LD_LIBRARY_PATH=/path/to/gdal/lib:$LD_LIBRARY_PATH

# 或在 Java 启动时指定
java -Djava.library.path=/path/to/gdal/lib -cp gdal.jar:app.jar MyApp

# Windows
set PATH=C:\gdal\bin;%PATH%
```

### 10.2 `gdal.jar` 与本地库版本不匹配

**症状**：`NoSuchMethodError` 或 `UnsatisfiedLinkError` 中出现方法签名不匹配。

**解决**：确保 `gdal.jar` 和 `gdalalljni.*` 来自**同一 GDAL 源代码版本**。Maven Central 上的 JAR 版本需与系统安装的 GDAL 主版本一致（如 jar 3.8.0 配 GDAL 3.8.x）。

### 10.3 Shapefile 中文乱码

**原因**：Shapefile 的 .dbf 部分通常使用本地编码（中国环境下多为 GBK/GB2312），而 Java 默认 UTF-8。

**解决**：

```java
// 打开前设置编码
gdal.SetConfigOption("SHAPE_ENCODING", "GBK");
// 或
gdal.SetConfigOption("SHAPE_ENCODING", "=GBK");  // 强制指定，不自动检测
```

### 10.4 内存泄漏（原生堆持续增长）

**原因**：SWIG 包装对象未及时 `delete()`，原生 C++ 对象堆积。

**解决**：

* 所有 `Dataset`、`Band`、`Feature`、`Geometry`、`SpatialReference`、`CoordinateTransformation` 对象在使用完毕后显式调用 `.delete()`
* 在 `finally` 块中确保释放
* 监控 RSS 内存（不仅是 Java 堆），可用 `jcmd <pid> VM.native_memory summary` 或操作系统工具

### 10.5 `GetRasterBand` 返回 null 或越界

**原因**：带号从 **1** 开始，传入 0 会返回 null。

```java
// 错误
Band b0 = ds.GetRasterBand(0);  // null!

// 正确
Band b1 = ds.GetRasterBand(1);  // 第一个波段
```

### 10.6 多线程下随机崩溃（段错误）

**原因**：GDAL 未以多线程模式编译，GC 线程与主线程竞争原生资源。

**解决**：重新编译 GDAL 时确保多线程支持开启（CMake 默认开启；autoconf 需加 `--with-threads`）。

### 10.7 打开远程文件超时

```java
gdal.SetConfigOption("CPL_VSIL_CURL_TIMEOUT", "60");      // 连接超时
gdal.SetConfigOption("CPL_VSIL_CURL_CONNECT_TIMEOUT", "10"); // 建连超时
gdal.SetConfigOption("CPL_VSIL_CURL_RETRY", "3");          // 重试次数
```

### 10.8 如何确认某个格式驱动是否可用

```java
gdal.AllRegister();
Driver driver = gdal.GetDriverByName("GTiff");
if (driver == null) {
    System.err.println("GTiff 驱动不可用，请检查 GDAL 编译选项");
}

// 列出所有可用栅格驱动
int driverCount = gdal.GetDriverCount();
for (int i = 0; i < driverCount; i++) {
    Driver d = gdal.GetDriver(i);
    System.out.println(d.getShortName() + " - " + d.getLongName());
}
```

***

## 11. 版本演进与兼容性

### 11.1 关键版本节点

| 版本 | 年份 | 重要变更 |
|------|------|----------|
| 1.7.0 | 2008 | Java 绑定首次随官方发布，Javadoc 开始覆盖 |
| 1.11.x | 2012 | 稳定期，Maven Central 最早收录的版本之一 |
| 2.0 | 2016 | OGR 引入 `OGRLayer::GetNextByIndex`，Java 同步更新 |
| 2.4 | 2018 | 引入 `SetFields` 批量属性写入；GeoJSON 驱动增强 |
| 3.0 | 2019 | **重大重构**：`ogr` 包重组，部分类迁移；废弃 `OGRGeometry::transform` 旧签名；要求 C++11 |
| 3.1 | 2020 | 新增 `ExecuteSQL` 支持更多方言；`BuildOverviews` 改进 |
| 3.4 | 2021 | `GDALWarp` 选项扩展；`/vsis3/` 改进 |
| 3.8 | 2023 | **JNI 库安装路径分离**（`GDAL_JAVA_JNI_INSTALL_DIR`）；`AUTODETECT_JSON_STRINGS` 等 GeoJSON 新选项 |
| 3.9+ | 2024+ | 持续优化，`FOREIGN_MEMBERS_FEATURE` 等 GeoJSON 高级特性 |

### 11.2 3.0 迁移注意事项

GDAL 3.0 是一次较大的 API 变动，Java 绑定受影响的主要变化：

1. **`ogr.Feature.SetGeometry` vs `SetGeometryDirectly`**：前者转移所有权（不再需要手动 delete 传入的 Geometry），后者不转移。3.0 后推荐使用 `SetGeometryDirectly` 以避免所有权混淆。
2. **`ogr.Driver.Open` 签名变化**：部分重载被废弃。
3. **`osr.SpatialReference` 方法重命名**：少数方法从 CamelCase 调整为更一致的命名。
4. **最低 Java 版本**：构建要求升至 JDK 7+（3.0 前为 JDK 6+）。

### 11.3 与第三方框架的集成

| 框架 | 集成方式 | 说明 |
|------|----------|------|
| **GeoServer** | Image I/O-Ext 插件 | GeoServer 通过 ImageIO-ext 的 gdalframework 模块调用 GDAL Java 绑定；需确保 gdal.jar 版本与服务器端 GDAL 匹配 |
| **Image I/O-Ext** | `gdalframework` 核心模块 | 提供丰富的格式支持框架，但不一定捆绑最新 GDAL |
| **Spring Boot** | 直接依赖 gdal.jar | 在 `application.yml` 中配置 `java.library.path` 或通过 `System.loadLibrary` 手动加载 |
| **Flink / Spark** | 自定义 Source/Sink | 利用 GDAL 读写能力构建分布式地理空间 ETL |

### 11.4 许可证

GDAL 采用 **MIT/X 许可协议**，允许商业使用、修改和再分发，仅需保留版权声明。Java 绑定同样遵循此许可。

***

## 12. 参考资料

### 官方文档

| 资源 | 地址 |
|------|------|
| GDAL 官网 | https://gdal.org |
| Java 绑定文档（官方） | https://gdal.org/api/java/ |
| Javadoc（API 参考） | https://gdal.org/java/ |
| GitHub 仓库 | https://github.com/OSGeo/gdal |
| Java 示例程序目录 | https://github.com/OSGeo/gdal/tree/master/swig/java/apps/ |
| Maven Central (org.gdal) | https://search.maven.org/search?q=g:org.gdal |
| 构建文档（Java 选项） | https://gdal.org/en/latest/development/building_from_source.html |

### 社区与工具

| 资源 | 说明 |
|------|------|
| Tamas Szekeres' Windows SDK | http://www.gisinternals.com/sdk — Win32/Win64 预编译包（含 Java 绑定） |
| OSGeo4W | https://trac.osgeo.org/osgeo4w/ — Windows 安装包管理器 |
| Image I/O-Ext | https://imageio-ext.dev.java.net/ — 基于 GDAL 绑定的 Java 图像 I/O 框架 |
| gdal-java (Gitee 中文示例) | https://gitee.com/zhong-liuyang/gdal-java — 中文注释的完整示例工程 |

### 相关书籍

* 《GDAL 源码剖析与开发指南》— 李民录，人民邮电出版社，ISBN 9787115338990
* 《GIS 开发核心技术——基于 GDAL 和 GeoServer》— 多篇技术文章汇编

***

> **文档生成时间**：2026-09-13
> **信息来源**：OSGeo/gdal 官方仓库（master 分支）、Context7 文档索引、Maven Central、公开技术资料
> **适用 GDAL 版本**：3.x 系列（重点覆盖 3.8+）
