第05章 - GDAL引擎架构设计
5.1 GDAL引擎概述
5.1.1 为什么选择GDAL
OGU4Net选择GDAL作为唯一的GIS引擎,这是经过深思熟虑的决策:
| 考量因素 | 说明 |
|---|---|
| 格式支持 | GDAL支持超过200种栅格和矢量格式 |
| 性能卓越 | C/C++底层实现,性能出色 |
| 跨平台 | 支持Windows、Linux、macOS等平台 |
| 社区活跃 | OSGeo基金会支持,社区活跃 |
| 行业标准 | GIS行业事实标准库 |
| NuGet支持 | MaxRev.Gdal.Universal提供优秀的.NET绑定 |
5.1.2 GDAL/OGR/OSR关系
GDAL实际上是一个库集合:
GDAL (Geospatial Data Abstraction Library)
├── GDAL Core - 栅格数据处理
├── OGR - 矢量数据处理 (Simple Features)
└── OSR - 空间参考系统 (Coordinate Systems)
OGU4Net主要使用:
- OGR:矢量数据读写、几何操作
- OSR:坐标系定义、坐标转换
5.1.3 MaxRev.Gdal.Universal
OGU4Net通过 MaxRev.Gdal.Universal 访问GDAL:
| 特性 | 说明 |
|---|---|
| 自动配置 | 自动处理GDAL库加载和配置 |
| 跨平台 | 包含所有平台的原生库 |
| 驱动内置 | 常用驱动已编译进包 |
| 持续更新 | 跟进GDAL新版本 |
5.2 GdalEngine实现
5.2.1 类结构
namespace OpenGIS.Utils.Engine;
/// <summary>
/// GDAL引擎实现
/// </summary>
public class GdalEngine : GisEngine
{
/// <summary>
/// 引擎类型
/// </summary>
public override GisEngineType EngineType => GisEngineType.GDAL;
/// <summary>
/// 支持的格式列表
/// </summary>
public override IList<DataFormatType> SupportedFormats => new List<DataFormatType>
{
DataFormatType.FILEGDB,
DataFormatType.GEOPACKAGE,
DataFormatType.KML,
DataFormatType.DXF,
DataFormatType.SHP,
DataFormatType.GEOJSON
}; // POSTGIS 经由 `PG:` 连接串驱动推断读写;TXT 由 GtTxtUtil 专门处理
/// <summary>
/// 创建读取器
/// </summary>
public override ILayerReader CreateReader()
{
return new GdalReader();
}
/// <summary>
/// 创建写入器
/// </summary>
public override ILayerWriter CreateWriter()
{
return new GdalWriter();
}
}
5.2.2 引擎工厂
namespace OpenGIS.Utils.Engine;
/// <summary>
/// GIS引擎工厂
/// </summary>
public static class GisEngineFactory
{
// 单例引擎实例
private static readonly GdalEngine _gdalEngineInstance = new();
/// <summary>
/// 根据引擎类型获取引擎
/// </summary>
public static GisEngine GetEngine(GisEngineType engineType)
{
return engineType switch
{
GisEngineType.GDAL => _gdalEngineInstance,
GisEngineType.GEOTOOLS => _gdalEngineInstance, // 兼容性重定向
_ => throw new EngineNotSupportedException(
$"Engine type {engineType} is not supported")
};
}
/// <summary>
/// 根据数据格式获取引擎
/// </summary>
public static GisEngine GetEngine(DataFormatType format)
{
// 所有格式都使用GDAL
return _gdalEngineInstance;
}
}
5.3 GdalReader深度解析
5.3.1 核心读取流程
public class GdalReader : ILayerReader
{
static GdalReader()
{
// 确保GDAL已初始化
GdalConfiguration.ConfigureGdal();
}
public OguLayer Read(
string path,
string? layerName = null,
string? attributeFilter = null,
string? spatialFilterWkt = null,
Dictionary<string, object>? options = null)
{
// 读取在进程级全局配置锁内串行执行:SHAPE_ENCODING 是进程级配置,
// 编码敏感的打开/读取需避免并发读取互相覆盖,读取结束后恢复原值
lock (GlobalConfigLock)
{
// 1. 参数验证
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Path cannot be null or empty", nameof(path));
// 应用 options["encoding"](Shapefile 属性编码)
ApplyEncodingOption(options);
OgrDataSource? dataSource = null;
try
{
// 2. 打开数据源
dataSource = Ogr.Open(path, 0); // 0 = 只读
if (dataSource == null)
throw new DataSourceException($"Failed to open data source: {path}");
// 3. 获取图层
Layer ogrLayer;
if (!string.IsNullOrWhiteSpace(layerName))
{
ogrLayer = dataSource.GetLayerByName(layerName);
if (ogrLayer == null)
throw new DataSourceException($"Layer '{layerName}' not found");
}
else
{
if (dataSource.GetLayerCount() == 0)
throw new DataSourceException("No layers found in data source");
ogrLayer = dataSource.GetLayerByIndex(0);
}
// 4. 读取并转换
return ReadOgrLayer(ogrLayer, attributeFilter, spatialFilterWkt);
}
finally
{
// 5. 释放资源并恢复 SHAPE_ENCODING
dataSource?.Dispose();
RestoreShapeEncoding();
}
}
}
}
5.3.2 读取OGR图层
private OguLayer ReadOgrLayer(
Layer ogrLayer,
string? attributeFilter,
string? spatialFilterWkt)
{
var layer = new OguLayer { Name = ogrLayer.GetName() };
// 1. 读取字段定义
var layerDefn = ogrLayer.GetLayerDefn();
var fieldCount = layerDefn.GetFieldCount();
for (int i = 0; i < fieldCount; i++)
{
var fieldDefn = layerDefn.GetFieldDefn(i);
var field = new OguField
{
Name = fieldDefn.GetName(),
DataType = MapOgrFieldType(fieldDefn.GetFieldType()),
Length = fieldDefn.GetWidth(),
Precision = fieldDefn.GetPrecision()
};
layer.AddField(field);
}
// 2. 确定几何类型
var geomType = ogrLayer.GetGeomType();
layer.GeometryType = MapOgrGeometryType(geomType);
// 3. 应用属性过滤(无效过滤器抛出 FormatParseException;
// GPKG 等 SQL 驱动把过滤器编译推迟到首次取要素,非法过滤同样包装为 FormatParseException)
if (!string.IsNullOrWhiteSpace(attributeFilter))
{
try
{
ogrLayer.SetAttributeFilter(attributeFilter);
}
catch (System.Exception ex)
{
throw new FormatParseException($"Invalid attribute filter: {attributeFilter}", ex);
}
}
// 4. 应用空间过滤(无效 WKT 抛出 FormatParseException,不静默忽略)
if (!string.IsNullOrWhiteSpace(spatialFilterWkt))
{
try
{
using var filterGeom = OSGeo.OGR.Geometry.CreateFromWkt(spatialFilterWkt);
if (filterGeom == null)
throw new FormatParseException($"Invalid spatial filter WKT: {spatialFilterWkt}");
ogrLayer.SetSpatialFilter(filterGeom);
}
catch (FormatParseException)
{
throw;
}
catch (System.Exception ex)
{
throw new FormatParseException($"Invalid spatial filter WKT: {spatialFilterWkt}", ex);
}
}
// 5. 读取要素(保留源数据 FID)
ogrLayer.ResetReading();
Feature? ogrFeature;
while ((ogrFeature = ogrLayer.GetNextFeature()) != null)
{
using (ogrFeature)
{
var feature = new OguFeature { Fid = (int)ogrFeature.GetFID() };
// 读取几何
var geometry = ogrFeature.GetGeometryRef();
if (geometry != null)
{
geometry.ExportToWkt(out string wkt);
feature.Wkt = wkt;
}
// 读取属性
foreach (var field in layer.Fields)
{
var fieldIndex = ogrFeature.GetFieldIndex(field.Name);
if (fieldIndex >= 0)
{
var value = GetFieldValue(ogrFeature, fieldIndex, field.DataType, field.Name);
feature.SetValue(field.Name, value);
}
}
layer.AddFeature(feature);
}
}
return layer;
}
5.3.3 字段值读取
private object? GetFieldValue(Feature feature, int fieldIndex, FieldDataType dataType, string fieldName)
{
// 检查字段是否已设置
if (!feature.IsFieldSet(fieldIndex))
return null;
// OGR 的 null 字段(如 Shapefile 空 'D' 日期字段)按空值返回,不再抛异常
if (feature.IsFieldNull(fieldIndex))
return null;
return dataType switch
{
FieldDataType.INTEGER => feature.GetFieldAsInteger(fieldIndex),
FieldDataType.LONG => feature.GetFieldAsInteger64(fieldIndex),
FieldDataType.DOUBLE or FieldDataType.FLOAT => feature.GetFieldAsDouble(fieldIndex),
FieldDataType.STRING => feature.GetFieldAsString(fieldIndex),
FieldDataType.DATE or FieldDataType.DATETIME => GetDateTimeValue(feature, fieldIndex, fieldName),
_ => feature.GetFieldAsString(fieldIndex)
};
}
private DateTime? GetDateTimeValue(Feature feature, int fieldIndex, string fieldName)
{
try
{
feature.GetFieldAsDateTime(
fieldIndex,
out int year, out int month, out int day,
out int hour, out int minute, out float second,
out int tzFlag);
// 保留 GDAL 返回的小数秒
var wholeSeconds = (int)Math.Truncate(second);
var fractionalTicks = (long)Math.Round(
(second - wholeSeconds) * TimeSpan.TicksPerSecond, MidpointRounding.AwayFromZero);
return new DateTime(year, month, day, hour, minute, wholeSeconds).AddTicks(fractionalTicks);
}
catch (System.Exception ex)
{
// 无法解析的日期字段以类型化异常报告,不再静默返回 null
throw new FormatParseException($"Invalid date field '{fieldName}'", ex);
}
}
5.3.4 获取图层名称列表
public IList<string> GetLayerNames(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Path cannot be null or empty", nameof(path));
using var dataSource = Ogr.Open(path, 0);
if (dataSource == null)
throw new DataSourceException($"Failed to open data source: {path}");
var layerNames = new List<string>();
var layerCount = dataSource.GetLayerCount();
for (int i = 0; i < layerCount; i++)
{
using var layer = dataSource.GetLayerByIndex(i);
if (layer != null)
{
layerNames.Add(layer.GetName());
}
}
return layerNames;
}
数据源打不开时抛出 DataSourceException,而不是静默返回空列表(空列表只表示数据源确实不含图层)。
5.4 GdalWriter深度解析
5.4.1 核心写入流程
public class GdalWriter : ILayerWriter
{
static GdalWriter()
{
GdalConfiguration.ConfigureGdal();
}
public void Write(
OguLayer layer,
string path,
string? layerName = null,
Dictionary<string, object>? options = null)
{
// 1. 参数验证
if (layer == null)
throw new ArgumentNullException(nameof(layer));
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Path cannot be null or empty", nameof(path));
// 2. 推断驱动名称
string driverName = InferDriverName(path, options);
var driver = Ogr.GetDriverByName(driverName);
if (driver == null)
throw new DataSourceException($"Driver '{driverName}' not available");
// 3. 确保目录存在
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 4. 删除已存在的文件(删除失败抛出 DataSourceException,而非忽略)
if (File.Exists(path) || Directory.Exists(path))
{
if (driver.DeleteDataSource(path) != 0)
throw new DataSourceException($"Failed to delete existing data source: {path}");
}
OgrDataSource? dataSource = null;
try
{
// 5. 创建数据源
dataSource = driver.CreateDataSource(path, new string[] { });
if (dataSource == null)
throw new DataSourceException($"Failed to create data source: {path}");
// 6. 创建图层:几何类型按首个可解析要素的 WKT 解析,
// PointZ/PolylineZ 等升级为对应 25D 类型(GPKG 不再出现"声明2D却含Z几何");
// 坐标系来自 layer.Wkid;图层创建选项包含 encoding/GeoJSON 坐标精度/OVERWRITE 等
var ogrGeomType = ResolveOgrGeometryType(layer);
var layerOptions = BuildLayerOptions(options, driverName);
using var spatialReference = CreateSpatialReference(layer.Wkid);
var ogrLayer = dataSource.CreateLayer(
layerName ?? layer.Name ?? "layer",
spatialReference,
ogrGeomType,
layerOptions);
if (ogrLayer == null)
throw new DataSourceException("Failed to create layer");
// 7. 创建字段:DXF 等固定 schema 驱动跳过失败字段并告警,
// 其余驱动创建失败即抛 DataSourceException;
// 字段索引按"实际创建顺序的序数"映射,
// 规避 GDAL 清洗字段名(如 Shapefile 截断超 10 字符名称)导致的按名查找失败
var createdFieldOrdinals = CreateFields(ogrLayer, layer, driverName);
var fieldIndexMap = BuildFieldIndexMap(layer, createdFieldOrdinals);
// 8. 写入要素:空几何要素跳过并计数告警;
// 非零源 FID 尽量保留(PostgreSQL 下 0 也显式保留),
// FID 冲突时改用自动分配重试一次;
// 失败要素汇总后以 DataSourceException 报告
foreach (var oguFeature in layer.Features)
{
WriteFeature(ogrLayer, oguFeature, layer.Fields, fieldIndexMap);
}
// 9. 同步到磁盘
dataSource.SyncToDisk();
}
finally
{
dataSource?.Dispose();
}
}
}
5.4.2 驱动名称推断
private string InferDriverName(string path, Dictionary<string, object>? options)
{
// 优先从选项获取
if (options != null && options.TryGetValue("driver", out var driverObj))
{
return driverObj.ToString() ?? "ESRI Shapefile";
}
// 数据库连接串没有文件扩展名,必须先按前缀识别,否则会误落默认 Shapefile 驱动
if (path.StartsWith("PG:", StringComparison.OrdinalIgnoreCase))
return "PostgreSQL";
// 根据扩展名推断
var extension = Path.GetExtension(path).ToLowerInvariant();
return extension switch
{
".shp" => "ESRI Shapefile",
".gdb" => "OpenFileGDB", // GDAL 3.6+ 可写,避免依赖 ESRI SDK 的 FileGDB 驱动
".gpkg" => "GPKG",
".kml" => "KML",
".dxf" => "DXF",
".geojson" or ".json" => "GeoJSON",
_ => "ESRI Shapefile"
};
}
5.4.3 创建OGR字段定义
private FieldDefn CreateOgrFieldDefn(OguField field, bool kmlDemoteDates = false)
{
var ogrType = MapToOgrFieldType(field.DataType);
// KML 规范无日期类型:OFTDate 会让原生 KML 写驱动整体失败,
// 写入 KML/LIBKML 时日期降级为文本列,内容以 ISO 串保留
if (kmlDemoteDates && field.DataType is FieldDataType.DATE or FieldDataType.DATETIME)
ogrType = FieldType.OFTString;
var fieldDefn = new FieldDefn(field.Name, ogrType);
// 设置字段宽度
if (field.Length.HasValue && field.Length.Value > 0)
{
fieldDefn.SetWidth(field.Length.Value);
}
// 设置精度
if (field.Precision.HasValue && field.Precision.Value > 0)
{
fieldDefn.SetPrecision(field.Precision.Value);
}
return fieldDefn;
}
private FieldType MapToOgrFieldType(FieldDataType dataType)
{
return dataType switch
{
FieldDataType.INTEGER => FieldType.OFTInteger,
FieldDataType.LONG => FieldType.OFTInteger64,
FieldDataType.DOUBLE or FieldDataType.FLOAT => FieldType.OFTReal,
FieldDataType.STRING => FieldType.OFTString,
FieldDataType.DATE => FieldType.OFTDate,
FieldDataType.DATETIME => FieldType.OFTDateTime,
FieldDataType.BINARY => FieldType.OFTBinary,
_ => FieldType.OFTString
};
}
5.4.4 写入要素
private void WriteFeature(Layer ogrLayer, OguFeature oguFeature, IList<OguField> fields,
Dictionary<string, int> fieldIndexMap)
{
// 空几何要素(如 Shapefile 的 NullShape 记录)跳过并汇总计数,不判为整层失败
if (string.IsNullOrWhiteSpace(oguFeature.Wkt))
{
skippedNoGeometry++;
return;
}
Feature? ogrFeature = null;
OSGeo.OGR.Geometry? geometry = null;
try
{
// 创建要素,尽量保留非零源 FID(PostgreSQL 驱动下 0 也显式保留)
ogrFeature = new Feature(ogrLayer.GetLayerDefn());
if ((oguFeature.Fid != 0 || preserveZeroFid) && ogrFeature.SetFID(oguFeature.Fid) != 0)
throw new System.Exception($"设置要素 FID 失败 (Fid={oguFeature.Fid})");
// 设置几何
geometry = OSGeo.OGR.Geometry.CreateFromWkt(oguFeature.Wkt);
if (geometry == null)
throw new System.Exception($"无法解析要素几何 (Fid={oguFeature.Fid})");
if (ogrFeature.SetGeometry(geometry) != 0)
throw new System.Exception($"设置要素几何失败 (Fid={oguFeature.Fid})");
// 设置属性(按预计算的字段序数映射,不受 GDAL 名称清洗影响)
foreach (var field in fields)
{
if (fieldIndexMap.TryGetValue(field.Name, out var fieldIndex))
{
var value = oguFeature.GetValue(field.Name);
SetFieldValue(ogrFeature, fieldIndex, value, field.DataType, field.Name);
}
}
// 添加到图层;FID 冲突(GPKG/OpenFileGDB UNIQUE 约束)时改用自动分配重试一次
if (!TryCreateFeature(ogrLayer, ogrFeature) && (oguFeature.Fid != 0 || preserveZeroFid))
{
ogrFeature.SetFID(-1);
TryCreateFeature(ogrLayer, ogrFeature);
}
}
catch (System.Exception ex)
{
// 单个要素失败不再 Console.WriteLine,而是计入失败数并以结构化日志告警,
// 处理完全部输入后汇总抛出 DataSourceException
failedCount++;
Logger.LogWarning(ex, "写入要素时出错 (Fid={Fid})", oguFeature.Fid);
}
finally
{
geometry?.Dispose();
ogrFeature?.Dispose();
}
}
private void SetFieldValue(Feature feature, int fieldIndex, object? value, FieldDataType dataType,
string fieldName)
{
if (value == null)
{
// 优先使用 OGR null 语义(GDAL 3.3+),GeoJSON 的 WRITE_NULL_FIELDS 等选项
// 依赖 null 而非 unset;旧绑定不支持时回退 UnsetField
try { feature.SetFieldNull(fieldIndex); }
catch (System.Exception) { feature.UnsetField(fieldIndex); }
return;
}
switch (dataType)
{
case FieldDataType.INTEGER:
feature.SetField(fieldIndex, Convert.ToInt32(value, CultureInfo.InvariantCulture));
break;
case FieldDataType.LONG:
feature.SetField(fieldIndex, Convert.ToInt64(value, CultureInfo.InvariantCulture));
break;
case FieldDataType.DOUBLE:
case FieldDataType.FLOAT:
feature.SetField(fieldIndex, Convert.ToDouble(value, CultureInfo.InvariantCulture));
break;
case FieldDataType.DATE:
case FieldDataType.DATETIME:
if (value is not DateTime dt)
throw new FormatException($"Value for date field '{fieldName}' is not a DateTime");
// 保留毫秒小数秒
var seconds = dt.Second + (float)dt.Millisecond / 1000;
feature.SetField(fieldIndex, dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, seconds, 0);
break;
default:
feature.SetField(fieldIndex, value.ToString());
break;
}
if (!feature.IsFieldSet(fieldIndex))
throw new DataSourceException($"Failed to set field '{fieldName}'");
}
5.5 驱动详解
5.5.1 查看可用驱动
using OpenGIS.Utils.Configuration;
// 获取所有驱动
var drivers = GdalConfiguration.GetSupportedDrivers();
Console.WriteLine($"Total drivers: {drivers.Count}");
foreach (var driver in drivers)
{
Console.WriteLine($" - {driver}");
}
// 检查特定驱动
Console.WriteLine($"\nDriver availability:");
Console.WriteLine($" ESRI Shapefile: {GdalConfiguration.IsDriverAvailable("ESRI Shapefile")}");
Console.WriteLine($" GeoJSON: {GdalConfiguration.IsDriverAvailable("GeoJSON")}");
Console.WriteLine($" GPKG: {GdalConfiguration.IsDriverAvailable("GPKG")}");
Console.WriteLine($" FileGDB: {GdalConfiguration.IsDriverAvailable("FileGDB")}");
Console.WriteLine($" OpenFileGDB: {GdalConfiguration.IsDriverAvailable("OpenFileGDB")}");
Console.WriteLine($" PostgreSQL: {GdalConfiguration.IsDriverAvailable("PostgreSQL")}");
5.5.2 常用驱动对照表
| 格式 | 驱动名称 | 读取 | 写入 | 说明 |
|---|---|---|---|---|
| Shapefile | ESRI Shapefile | ✅ | ✅ | 最常用格式 |
| GeoJSON | GeoJSON | ✅ | ✅ | Web标准格式 |
| GeoPackage | GPKG | ✅ | ✅ | OGC标准 |
| FileGDB | FileGDB | ✅ | ✅ | 依赖 ESRI FileGDB SDK,运行时可能缺失 |
| FileGDB | OpenFileGDB | ✅ | ✅ | 无需SDK;GDAL 3.6+ 支持创建,OGU4Net 写 .gdb 默认使用该驱动 |
| PostGIS | PostgreSQL | ✅ | ✅ | 需要连接字符串 |
| KML | KML | ✅ | ✅ | Google Earth |
| DXF | DXF | ✅ | ✅ | AutoCAD交换格式 |
| CSV | CSV | ✅ | ✅ | 文本表格 |
| GPX | GPX | ✅ | ✅ | GPS交换格式 |
5.5.3 特殊格式处理
PostGIS连接:
// PostGIS连接字符串格式
string connStr = "PG:host=localhost dbname=mydb user=postgres password=secret";
// 读取PostGIS表
var layer = OguLayerUtil.ReadLayer(
DataFormatType.POSTGIS,
connStr,
layerName: "public.my_table"
);
FileGDB处理:
// 检查FileGDB驱动
bool hasFileGDB = GdalConfiguration.IsDriverAvailable("FileGDB");
bool hasOpenFileGDB = GdalConfiguration.IsDriverAvailable("OpenFileGDB");
if (hasOpenFileGDB)
{
// GDAL 3.6+ 起 OpenFileGDB 可读写,GdalWriter 对 .gdb 默认选择该驱动
var layer = OguLayerUtil.ReadLayer(DataFormatType.FILEGDB, "data.gdb", "LayerName");
}
else if (hasFileGDB)
{
// 依赖 ESRI SDK 的 FileGDB 驱动
Console.WriteLine("FileGDB driver is available");
}
else
{
Console.WriteLine("No FileGDB driver available");
}
5.6 数据过滤
5.6.1 属性过滤
// SQL WHERE子句语法
var layer = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
attributeFilter: "Population > 1000000"
);
// 复杂条件
var filtered = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
attributeFilter: "Province = '广东省' AND Population > 500000"
);
// 字符串匹配
var match = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
attributeFilter: "Name LIKE '%市%'"
);
5.6.2 空间过滤
// 使用矩形范围过滤
var bbox = "POLYGON ((116.0 39.0, 117.0 39.0, 117.0 40.0, 116.0 40.0, 116.0 39.0))";
var layer = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
spatialFilterWkt: bbox
);
// 使用圆形范围过滤
var circle = GeometryUtil.BufferWkt("POINT (116.404 39.915)", 0.5);
var nearbyLayer = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
spatialFilterWkt: circle
);
5.6.3 组合过滤
// 同时使用属性和空间过滤
var layer = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"cities.shp",
attributeFilter: "Population > 500000",
spatialFilterWkt: "POLYGON ((116.0 39.0, 117.0 39.0, 117.0 40.0, 116.0 40.0, 116.0 39.0))"
);
5.7 格式转换
5.7.1 基本转换
// Shapefile → GeoJSON
OguLayerUtil.ConvertFormat(
"input.shp",
DataFormatType.SHP,
"output.geojson",
DataFormatType.GEOJSON
);
// GeoJSON → GeoPackage
OguLayerUtil.ConvertFormat(
"input.geojson",
DataFormatType.GEOJSON,
"output.gpkg",
DataFormatType.GEOPACKAGE
);
5.7.2 带处理的转换
// 1. 读取
var layer = OguLayerUtil.ReadLayer(DataFormatType.SHP, "input.shp");
// 2. 处理
// 例如:坐标转换
foreach (var feature in layer.Features)
{
if (!string.IsNullOrEmpty(feature.Wkt))
{
feature.Wkt = CrsUtil.Transform(feature.Wkt, 4326, 4490);
}
}
layer.Wkid = 4490;
// 3. 写入
OguLayerUtil.WriteLayer(DataFormatType.GEOJSON, layer, "output.geojson");
5.7.3 批量转换
var inputDir = "input/";
var outputDir = "output/";
var shpFiles = Directory.GetFiles(inputDir, "*.shp");
foreach (var shpFile in shpFiles)
{
var fileName = Path.GetFileNameWithoutExtension(shpFile);
var outputFile = Path.Combine(outputDir, $"{fileName}.geojson");
Console.WriteLine($"Converting: {fileName}");
OguLayerUtil.ConvertFormat(
shpFile, DataFormatType.SHP,
outputFile, DataFormatType.GEOJSON
);
}
5.8 性能优化
5.8.1 大文件处理
// 使用空间索引加速查询
// GDAL会自动使用.shx等索引文件
// 对于大数据集,使用过滤减少读取量
var layer = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
"large_dataset.shp",
attributeFilter: "Type = 'City'", // 减少读取量
spatialFilterWkt: bbox // 空间过滤
);
5.8.2 内存管理
// 分批处理大量数据
void ProcessLargeDataset(string inputPath, int batchSize = 1000)
{
using var dataSource = Ogr.Open(inputPath, 0);
var ogrLayer = dataSource.GetLayerByIndex(0);
ogrLayer.ResetReading();
var batch = new List<OguFeature>();
Feature? ogrFeature;
while ((ogrFeature = ogrLayer.GetNextFeature()) != null)
{
using (ogrFeature)
{
var feature = ConvertFeature(ogrFeature);
batch.Add(feature);
if (batch.Count >= batchSize)
{
ProcessBatch(batch);
batch.Clear();
GC.Collect(); // 适当回收内存
}
}
}
if (batch.Count > 0)
{
ProcessBatch(batch);
}
}
5.8.3 并行处理
// 异步读取多个文件
async Task<OguLayer[]> ReadMultipleFilesAsync(string[] paths)
{
var tasks = paths.Select(path =>
OguLayerUtil.ReadLayerAsync(DataFormatType.SHP, path));
return await Task.WhenAll(tasks);
}
// 使用示例
var paths = new[] { "file1.shp", "file2.shp", "file3.shp" };
var layers = await ReadMultipleFilesAsync(paths);
注意:
GdalReader的读取操作在进程级配置锁内串行执行(保护SHAPE_ENCODING等全局配置),并行读取不会带来吞吐提升;async版本仅用于避免阻塞调用线程。
5.9 错误处理
5.9.1 常见错误处理
try
{
var layer = OguLayerUtil.ReadLayer(DataFormatType.SHP, "data.shp");
}
catch (ArgumentException ex)
{
Console.WriteLine($"参数错误: {ex.Message}");
}
catch (DataSourceException ex)
{
// 数据源打不开、图层不存在等(类型化异常,不再依赖消息文本匹配)
Console.WriteLine($"无法打开数据源: {ex.Message}");
}
catch (FormatParseException ex)
{
// 无效的属性/空间过滤器、无法解析的日期字段等
Console.WriteLine($"解析失败: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
}
5.9.2 驱动检查
public void SafeWrite(OguLayer layer, string path, string driverName)
{
// 检查驱动可用性
if (!GdalConfiguration.IsDriverAvailable(driverName))
{
throw new EngineNotSupportedException(
$"Driver '{driverName}' is not available. " +
$"Available drivers: {string.Join(", ", GdalConfiguration.GetSupportedDrivers().Take(10))}...");
}
// 执行写入
var options = new Dictionary<string, object> { { "driver", driverName } };
OguLayerUtil.WriteLayer(DataFormatType.SHP, layer, path, options: options);
}
5.10 小结
本章详细介绍了OGU4Net的GDAL引擎架构:
- GDAL选择:行业标准、格式全面、性能优秀
- GdalEngine:引擎实现,创建读写器
- GdalReader:数据读取、过滤、类型映射
- GdalWriter:数据写入、驱动推断、字段创建
- 驱动系统:多种格式支持、特殊格式处理
- 性能优化:大文件处理、内存管理、并行处理
理解GDAL引擎的实现细节,有助于解决实际开发中遇到的问题,并进行必要的扩展和优化。