【Android】基于GDAL库实现SHP文件读写

前言

在数字化施工、测绘地理信息、轨迹采集等Android应用开发中,SHP(ESRI Shapefile)作为主流的地理空间数据格式,是存储点位、矢量数据的常用选择。而GDAL(Geospatial Data Abstraction Library)作为强大的开源地理空间数据处理库,能高效实现SHP文件的创建、导出与解析。

一、前期准备:GDAL库集成与基础配置

1.1 GDAL核心认知

GDAL是跨平台的地理空间数据转换库,支持SHP、GeoTIFF等数百种格式,在Android端需通过JNI调用底层so库实现功能。针对SHP文件处理,我们主要用到OGR库(GDAL的矢量数据处理模块),负责矢量数据的读写、图层管理、要素解析等。

1.2 Android端GDAL集成

首先需将编译好的GDAL相关so库放入Android项目的src/main/jniLibs目录下,对应架构按需放入(arm64-v8a、armeabi-v7a等),核心so库包含:

同时在项目中引入GDAL相关Java依赖(或AAR包),确保能正常调用org.gdal包下的API。

1.3 权限配置

SHP文件读写涉及文件存储操作,Android 6.0以上需动态申请存储权限,Android 10以上适配分区存储,本篇代码采用APP专属外部目录,无需额外申请MANAGE_EXTERNAL_STORAGE权限,兼容性更强。

二、整体架构:单例工具类设计

考虑到GDAL初始化耗时、资源占用较高,我们采用单例模式设计工具类,避免重复创建实例;同时封装中断机制,支持大数量轨迹点导出时的中断操作,提升用户体验。工具类核心结构:

  • 单例实现:静态内部类单例,保证全局唯一

  • GDAL初始化:加载so库、注册驱动,仅执行一次

  • 轨迹点模型:封装北坐标、东坐标、高程、点ID等字段

  • 核心功能:SHP文件导出、SHP文件读取

  • 辅助方法:文件清理、异常处理、资源释放

三、核心功能一:SHP文件导出(轨迹点生成SHP)

3.1 导出逻辑梳理

轨迹点SHP导出的核心流程:参数校验→文件预处理(清理旧文件)→GDAL配置→创建数据源→定义坐标系→创建图层→添加属性字段→写入点位数据→资源释放→同步磁盘。

3.2 核心代码详解

(1)轨迹点数据模型

封装轨迹点核心信息,适配读写场景,新增pointId字段对应SHP属性字段,重写toString方便调试:

java 复制代码
public static class TrackPoint {
    public int pointId;       // 点序号
    public double north;      // 北坐标
    public double east;       // 东坐标
    public double height;     // 高程

    public TrackPoint() {
    }

    public TrackPoint(double north, double east, double height) {
        this.north = north;
        this.east = east;
        this.height = height;
    }

    public TrackPoint(int pointId, double north, double east, double height) {
        this.pointId = pointId;
        this.north = north;
        this.east = east;
        this.height = height;
    }

    @Override
    public String toString() {
        return "TrackPoint{" +
                "pointId=" + pointId +
                ", north=" + north +
                ", east=" + east +
                ", height=" + height +
                '}';
    }
}
(2)GDAL初始化

按依赖顺序加载so库,注册所有OGR驱动,避免库加载顺序异常导致的崩溃:

java 复制代码
private static void initGdal() {
    try {
        // 按依赖顺序加载so库,不可调换
        System.loadLibrary("proj");
        System.loadLibrary("jsqlite");
        System.loadLibrary("gdalconstjni");
        System.loadLibrary("gdaljni");
        System.loadLibrary("osrjni");
        System.loadLibrary("ogrjni");

        // 注册所有矢量驱动
        ogr.RegisterAll();
        Log.d(TAG, "GDAL初始化成功,版本:" + gdal.VersionInfo());
    } catch (UnsatisfiedLinkError e) {
        Log.e(TAG, "GDAL加载so库失败", e);
        throw new RuntimeException("GDAL初始化失败,请检查so库配置");
    }
}
(3)SHP导出核心方法

处理文件创建、字段定义、数据写入,支持中文路径与属性,加入中断机制:

java 复制代码
private boolean writeShpWithGdal(String shpPath, List<TrackPoint> points) {
    // 配置GDAL编码,解决中文乱码
    gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8", "YES");
    gdal.SetConfigOption("SHAPE_ENCODING", "GBK");

    // 获取SHP驱动
    Driver shpDriver = ogr.GetDriverByName(SHP_DRIVER_NAME);
    if (shpDriver == null) {
        Log.e(TAG, "获取ESRI Shapefile驱动失败");
        return false;
    }

    // 创建SHP数据源
    DataSource dataSource = shpDriver.CreateDataSource(shpPath);
    if (dataSource == null) {
        Log.e(TAG, "创建SHP数据源失败:" + shpPath);
        return false;
    }

    // 定义WGS84坐标系
    SpatialReference srs = new SpatialReference();
    srs.SetWellKnownGeogCS(DEFAULT_SRS);

    // 创建点类型图层
    Layer layer = dataSource.CreateLayer("TrackPoints", srs, ogr.wkbPoint);
    if (layer == null) {
        Log.e(TAG, "创建SHP图层失败");
        dataSource.delete();
        return false;
    }

    // 定义属性字段:点ID、北坐标、东坐标、高程
    FieldDefn fieldId = new FieldDefn("POINT_ID", ogr.OFTInteger);
    layer.CreateField(fieldId);

    FieldDefn fieldNorth = new FieldDefn("NORTH", ogr.OFTReal);
    fieldNorth.SetPrecision(6);
    fieldNorth.SetWidth(15);
    layer.CreateField(fieldNorth);

    FieldDefn fieldEast = new FieldDefn("EAST", ogr.OFTReal);
    fieldEast.SetPrecision(6);
    fieldEast.SetWidth(15);
    layer.CreateField(fieldEast);

    FieldDefn fieldHeight = new FieldDefn("HEIGHT", ogr.OFTReal);
    fieldHeight.SetPrecision(6);
    fieldHeight.SetWidth(15);
    layer.CreateField(fieldHeight);

    // 写入轨迹点数据
    FeatureDefn featureDefn = layer.GetLayerDefn();
    for (int i = 0; i < points.size(); i++) {
        if (isInterrupted.get()) {
            Log.w(TAG, "导出被中断");
            dataSource.delete();
            return false;
        }

        TrackPoint point = points.get(i);
        Feature feature = new Feature(featureDefn);
        // 设置属性值
        feature.SetField("POINT_ID", i + 1);
        feature.SetField("NORTH", point.north);
        feature.SetField("EAST", point.east);
        feature.SetField("HEIGHT", point.height);

        // 创建点几何,X=东坐标,Y=北坐标
        Geometry pointGeom = new Geometry(ogr.wkbPoint);
        pointGeom.SetPoint_2D(0, point.east, point.north);
        feature.SetGeometry(pointGeom);

        // 添加要素到图层
        if (layer.CreateFeature(feature) != 0) {
            Log.e(TAG, "写入第" + (i+1) + "个点失败");
            feature.delete();
            dataSource.delete();
            return false;
        }
        feature.delete();
    }

    // 数据同步到磁盘,必调用
    layer.SyncToDisk();
    dataSource.delete();
    Log.d(TAG, "GDAL导出SHP成功:" + shpPath);
    return true;
}

四、核心功能二:SHP文件读取(解析轨迹点数据)

4.1 读取逻辑梳理

SHP文件读取核心流程:文件校验→GDAL配置→打开数据源→获取图层→遍历要素→解析属性字段+几何坐标→封装数据→资源释放,兼顾容错性,支持字段缺失时从几何对象补全数据。

4.2 核心代码详解

java 复制代码
public List<TrackPoint> readTrackFromShp(String shpFilePath) {
    List<TrackPoint> trackPoints = new ArrayList<>();
    // 校验文件是否存在
    File shpFile = new File(shpFilePath);
    if (!shpFile.exists()) {
        Log.e(TAG, "SHP文件不存在:" + shpFilePath);
        return trackPoints;
    }

    try {
        // 配置GDAL编码
        gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8", "YES");
        gdal.SetConfigOption("SHAPE_ENCODING", "GBK");

        // 打开SHP数据源
        DataSource dataSource = ogr.Open(shpFilePath);
        if (dataSource == null) {
            Log.e(TAG, "打开SHP数据源失败:" + shpFilePath);
            return trackPoints;
        }

        // 获取第一个图层(SHP默认单图层)
        Layer layer = dataSource.GetLayer(0);
        if (layer == null) {
            Log.e(TAG, "获取SHP图层失败");
            dataSource.delete();
            return trackPoints;
        }

        // 遍历所有要素
        layer.ResetReading();
        Feature feature;
        while ((feature = layer.GetNextFeature()) != null) {
            try {
                TrackPoint trackPoint = new TrackPoint();
                // 解析属性字段
                if (feature.IsFieldSet("POINT_ID")) {
                    trackPoint.pointId = feature.GetFieldAsInteger("POINT_ID");
                }
                if (feature.IsFieldSet("NORTH")) {
                    trackPoint.north = feature.GetFieldAsDouble("NORTH");
                }
                if (feature.IsFieldSet("EAST")) {
                    trackPoint.east = feature.GetFieldAsDouble("EAST");
                }
                if (feature.IsFieldSet("HEIGHT")) {
                    trackPoint.height = feature.GetFieldAsDouble("HEIGHT");
                }

                // 几何坐标备用,防止属性字段缺失
                Geometry geom = feature.GetGeometryRef();
                if (geom != null && geom.GetGeometryType() == ogr.wkbPoint) {
                    double eastFromGeom = geom.GetX();
                    double northFromGeom = geom.GetY();
                    // 字段缺失时补全坐标
                    if (!feature.IsFieldSet("EAST")) trackPoint.east = eastFromGeom;
                    if (!feature.IsFieldSet("NORTH")) trackPoint.north = northFromGeom;
                }
                trackPoints.add(trackPoint);
            } catch (Exception e) {
                Log.e(TAG, "解析要素失败", e);
            } finally {
                // 释放要素资源,避免内存泄漏
                feature.delete();
            }
        }
        dataSource.delete();
        Log.d(TAG, "读取SHP文件成功,共读取" + trackPoints.size() + "个轨迹点");
        return trackPoints;
    } catch (Exception e) {
        Log.e(TAG, "读取SHP文件失败", e);
        return trackPoints;
    }
}

五、工具类使用示例

5.1 初始化工具类

建议在Application中初始化,避免重复初始化:

java 复制代码
// 在Application的onCreate中调用
GdalShpTrackExporter.init(getApplicationContext());

5.2 导出SHP文件

java 复制代码
// 获取工具类实例
GdalShpTrackExporter exporter = GdalShpTrackExporter.getInstance();
// 构造轨迹点数据
List<GdalShpTrackExporter.TrackPoint> trackPoints = new ArrayList<>();
trackPoints.add(new GdalShpTrackExporter.TrackPoint(3856231.256, 425689.124, 85.623));
trackPoints.add(new GdalShpTrackExporter.TrackPoint(3856235.365, 425692.458, 86.125));
// 导出到默认目录
boolean isSuccess = exporter.exportTrackToShp("施工轨迹_001", trackPoints);
if (isSuccess) {
    Toast.makeText(this, "SHP导出成功", Toast.LENGTH_SHORT).show();
}

5.3 读取SHP文件

java 复制代码
// 获取工具类实例
GdalShpTrackExporter exporter = GdalShpTrackExporter.getInstance();
// 指定SHP文件路径
File shpFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "SHP_Export/施工轨迹_001.shp");
// 解析SHP数据
List<GdalShpTrackExporter.TrackPoint> points = exporter.readTrackFromShp(shpFile);
// 遍历解析结果
for (GdalShpTrackExporter.TrackPoint point : points) {
    Log.d("SHP解析", "点ID:" + point.pointId + " 北坐标:" + point.north + " 东坐标:" + point.east + " 高程:" + point.height);
}

六、常见坑点与优化建议

6.1 必避坑点

  • so库加载顺序:必须按proj→jsqlite→gdal相关→ogr相关的顺序加载,否则会报UnsatisfiedLinkError

  • 资源泄漏:Feature、DataSource、Geometry使用后必须调用delete()释放,否则会导致内存溢出

  • 中文乱码:必须配置GDAL_FILENAME_IS_UTF8和SHAPE_ENCODING,否则中文文件名/属性乱码

  • 文件覆盖:导出前需清理旧的shp/shx/dbf文件,否则GDAL创建数据源失败

  • 坐标系适配:几何坐标X对应东坐标、Y对应北坐标,切勿写反导致点位偏移

6.2 优化方案

  • 大数量点位读写建议放在子线程,避免阻塞主线程

  • 增加异常捕获,避免GDAL底层异常导致APP崩溃

  • 按需拓展字段,支持线、面类型SHP文件,适配更多地理数据场景

  • 加入进度回调,提升大批量数据处理的交互体验


总结

本文基于GDAL库实现了Android端SHP文件的完整读写方案,从集成配置、代码设计到实战调用、避坑优化全覆盖,适配测绘、数字化施工等地理信息类应用。该工具类采用单例设计,稳定性高、兼容性强,既能满足轨迹点SHP导出,也能高效解析SHP空间数据,大家可根据业务需求拓展字段、数据类型,适配更多场景。

相关动态库已经提交

相关推荐
冬奇Lab2 小时前
Android系统核心服务协作:从点击图标到应用显示的完整链路
android·源码阅读
fengci.2 小时前
ISCTF2021
android
ego.iblacat2 小时前
在 LNMP 平台中部署 Web 应用
android·前端·adb
一起搞IT吧2 小时前
Android功耗系列专题理论之十五:相机camera功耗问题分析方法
android·c++·数码相机·智能手机·性能优化
tntlbb2 小时前
苍穹外卖Day1:项目数据库连接问题排查与原理分析报告
android·adb
这个Bug有点难搞3 小时前
Android开发 JNI-调用第三方so库
android
2501_915106323 小时前
如何在 Mac 上面代理抓包和数据流分析
android·macos·ios·小程序·uni-app·iphone·webview
诸神黄昏EX3 小时前
Android Safety 系列专题【篇六:SecureElement安全硬件】
android
一只特立独行的Yang4 小时前
Android Graphics - openGL and Vulkan小结
android