前言
在数字化施工、测绘地理信息、轨迹采集等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库包含:
-
libproj.so:投影转换核心库
-
libjsqlite.so:数据库支持库
-
libgdalconstjni.so、libgdaljni.so:GDAL核心JNI库
-
libosrjni.so、libogrjni.so:OGR矢量处理库
同时在项目中引入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空间数据,大家可根据业务需求拓展字段、数据类型,适配更多场景。
相关动态库已经提交