第12章 - 扩展开发指南
第12章 - 扩展开发指南
12.1 扩展开发概述
OGU4Net采用模块化设计,支持多种扩展方式:
| 扩展类型 | 描述 | 难度 |
|---|---|---|
| 添加新数据格式 | 支持新的GIS数据格式 | 中等 |
| 添加新引擎 | 集成其他GIS库 | 较高 |
| 添加新工具类 | 扩展工具函数 | 简单 |
| 自定义异常 | 添加特定异常类型 | 简单 |
| 扩展模型 | 扩展OguLayer等模型 | 中等 |
12.2 添加新数据格式
12.2.1 扩展DataFormatType枚举
// 在项目中定义扩展枚举(不直接修改OGU4Net源码)
public static class CustomDataFormat
{
public const int CSV = 100;
public const int GPX = 101;
public const int TAB = 102; // MapInfo TAB
}
12.2.2 创建格式处理器
public interface IFormatHandler
{
OguLayer Read(string path, Dictionary<string, object>? options = null);
void Write(OguLayer layer, string path, Dictionary<string, object>? options = null);
}
public class CsvHandler : IFormatHandler
{
public OguLayer Read(string path, Dictionary<string, object>? options = null)
{
var layer = new OguLayer
{
Name = Path.GetFileNameWithoutExtension(path),
GeometryType = GeometryType.POINT
};
var lines = File.ReadAllLines(path);
if (lines.Length == 0) return layer;
// 解析表头
var headers = lines[0].Split(',').Select(h => h.Trim()).ToArray();
// 查找经纬度列
int lonIndex = Array.FindIndex(headers, h =>
h.Equals("lon", StringComparison.OrdinalIgnoreCase) ||
h.Equals("longitude", StringComparison.OrdinalIgnoreCase) ||
h.Equals("x", StringComparison.OrdinalIgnoreCase));
int latIndex = Array.FindIndex(headers, h =>
h.Equals("lat", StringComparison.OrdinalIgnoreCase) ||
h.Equals("latitude", StringComparison.OrdinalIgnoreCase) ||
h.Equals("y", StringComparison.OrdinalIgnoreCase));
if (lonIndex < 0 || latIndex < 0)
{
throw new FormatException("CSV文件必须包含经纬度列(lon/lat或x/y)");
}
// 添加字段(排除经纬度列)
for (int i = 0; i < headers.Length; i++)
{
if (i != lonIndex && i != latIndex)
{
layer.AddField(new OguField
{
Name = headers[i],
DataType = FieldDataType.STRING,
Length = 254
});
}
}
// 读取数据
for (int row = 1; row < lines.Length; row++)
{
var values = lines[row].Split(',');
if (values.Length < headers.Length) continue;
if (!double.TryParse(values[lonIndex].Trim(), out double lon) ||
!double.TryParse(values[latIndex].Trim(), out double lat))
{
continue;
}
var feature = new OguFeature
{
Fid = row,
Wkt = $"POINT ({lon} {lat})"
};
for (int i = 0; i < headers.Length; i++)
{
if (i != lonIndex && i != latIndex)
{
feature.SetValue(headers[i], values[i].Trim());
}
}
layer.AddFeature(feature);
}
return layer;
}
public void Write(OguLayer layer, string path, Dictionary<string, object>? options = null)
{
var lines = new List<string>();
// 写表头
var fieldNames = layer.Fields.Select(f => f.Name).ToList();
fieldNames.Insert(0, "longitude");
fieldNames.Insert(1, "latitude");
lines.Add(string.Join(",", fieldNames));
// 写数据
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
var geom = GeometryUtil.Wkt2Geometry(feature.Wkt);
var centroid = GeometryUtil.Centroid(geom);
double lon = centroid.GetX(0);
double lat = centroid.GetY(0);
var values = new List<string>
{
lon.ToString(),
lat.ToString()
};
foreach (var field in layer.Fields)
{
var value = feature.GetValue(field.Name)?.ToString() ?? "";
// 处理CSV特殊字符
if (value.Contains(',') || value.Contains('"'))
{
value = $"\"{value.Replace("\"", "\"\"")}\"";
}
values.Add(value);
}
lines.Add(string.Join(",", values));
}
File.WriteAllLines(path, lines, Encoding.UTF8);
}
}
12.2.3 注册格式处理器
public static class FormatRegistry
{
private static readonly Dictionary<string, IFormatHandler> _handlers = new()
{
{ ".csv", new CsvHandler() },
// 添加更多处理器
};
public static void Register(string extension, IFormatHandler handler)
{
_handlers[extension.ToLower()] = handler;
}
public static IFormatHandler? GetHandler(string path)
{
var ext = Path.GetExtension(path).ToLower();
return _handlers.TryGetValue(ext, out var handler) ? handler : null;
}
public static OguLayer ReadFile(string path)
{
var handler = GetHandler(path);
if (handler == null)
{
throw new NotSupportedException($"不支持的文件格式: {Path.GetExtension(path)}");
}
return handler.Read(path);
}
public static void WriteFile(OguLayer layer, string path)
{
var handler = GetHandler(path);
if (handler == null)
{
throw new NotSupportedException($"不支持的文件格式: {Path.GetExtension(path)}");
}
handler.Write(layer, path);
}
}
12.3 添加新GIS引擎
12.3.1 定义新引擎类型
// 扩展引擎类型
public enum CustomEngineType
{
NETOPOLOGYSUITE = 10,
DOTSPATIAL = 11
}
12.3.2 实现新引擎
// 基于NetTopologySuite的引擎示例
public class NtsEngine : GisEngine
{
public override GisEngineType EngineType => (GisEngineType)CustomEngineType.NETOPOLOGYSUITE;
public override IList<DataFormatType> SupportedFormats => new List<DataFormatType>
{
DataFormatType.SHP,
DataFormatType.GEOJSON
};
public override ILayerReader CreateReader()
{
return new NtsLayerReader();
}
public override ILayerWriter CreateWriter()
{
return new NtsLayerWriter();
}
}
public class NtsLayerReader : ILayerReader
{
public OguLayer Read(
string path,
string? layerName = null,
string? attributeFilter = null,
string? spatialFilterWkt = null,
Dictionary<string, object>? options = null)
{
// 使用NetTopologySuite读取
// 这里是伪代码,实际需要实现
throw new NotImplementedException("NTS读取器待实现");
}
public IList<string> GetLayerNames(string path)
{
return new List<string> { Path.GetFileNameWithoutExtension(path) };
}
}
public class NtsLayerWriter : ILayerWriter
{
public void Write(
OguLayer layer,
string path,
string? layerName = null,
Dictionary<string, object>? options = null)
{
throw new NotImplementedException("NTS写入器待实现");
}
public void Append(
OguLayer layer,
string path,
string? layerName = null,
Dictionary<string, object>? options = null)
{
throw new NotImplementedException("NTS追加器待实现");
}
}
12.3.3 注册新引擎
public static class EngineRegistry
{
private static readonly Dictionary<int, GisEngine> _engines = new();
static EngineRegistry()
{
// 注册默认引擎
Register((int)GisEngineType.GDAL, new GdalEngine());
}
public static void Register(int engineType, GisEngine engine)
{
_engines[engineType] = engine;
}
public static GisEngine GetEngine(int engineType)
{
if (_engines.TryGetValue(engineType, out var engine))
{
return engine;
}
throw new EngineNotSupportedException($"引擎类型 {engineType} 未注册");
}
}
12.4 扩展OguLayer模型
12.4.1 使用继承扩展
public class ExtendedOguLayer : OguLayer
{
/// <summary>
/// 空间索引
/// </summary>
public ISpatialIndex? SpatialIndex { get; private set; }
/// <summary>
/// 构建空间索引
/// </summary>
public void BuildSpatialIndex()
{
// 使用R-Tree构建空间索引
// 这里是概念示例
SpatialIndex = new RTreeIndex();
foreach (var feature in Features)
{
if (!string.IsNullOrEmpty(feature.Wkt))
{
var envelope = GetEnvelope(feature.Wkt);
SpatialIndex.Insert(envelope, feature.Fid);
}
}
}
/// <summary>
/// 空间查询
/// </summary>
public IList<OguFeature> SpatialQuery(string envelopeWkt)
{
if (SpatialIndex == null)
{
throw new InvalidOperationException("请先调用 BuildSpatialIndex()");
}
var envelope = GetEnvelope(envelopeWkt);
var fids = SpatialIndex.Query(envelope);
return Features.Where(f => fids.Contains(f.Fid)).ToList();
}
private Envelope GetEnvelope(string wkt)
{
var geom = GeometryUtil.Wkt2Geometry(wkt);
var envGeom = GeometryUtil.Envelope(geom);
// 返回Envelope对象
return new Envelope(); // 简化示例
}
}
12.4.2 使用扩展方法
public static class OguLayerExtensions
{
/// <summary>
/// 计算图层边界
/// </summary>
public static string? GetBounds(this OguLayer layer)
{
if (layer.Features.Count == 0) return null;
double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue;
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
var geom = GeometryUtil.Wkt2Geometry(feature.Wkt);
var env = new OSGeo.OGR.Envelope();
geom.GetEnvelope(env);
if (env.MinX < minX) minX = env.MinX;
if (env.MinY < minY) minY = env.MinY;
if (env.MaxX > maxX) maxX = env.MaxX;
if (env.MaxY > maxY) maxY = env.MaxY;
}
return $"POLYGON (({minX} {minY}, {maxX} {minY}, {maxX} {maxY}, {minX} {maxY}, {minX} {minY}))";
}
/// <summary>
/// 按字段分组
/// </summary>
public static Dictionary<string, OguLayer> GroupByField(this OguLayer layer, string fieldName)
{
var groups = new Dictionary<string, OguLayer>();
foreach (var feature in layer.Features)
{
var key = feature.GetValue(fieldName)?.ToString() ?? "NULL";
if (!groups.ContainsKey(key))
{
groups[key] = new OguLayer
{
Name = $"{layer.Name}_{key}",
GeometryType = layer.GeometryType,
Wkid = layer.Wkid
};
foreach (var field in layer.Fields)
{
groups[key].AddField(field.Clone());
}
}
groups[key].AddFeature(feature.Clone());
}
return groups;
}
/// <summary>
/// 导出为GeoJSON FeatureCollection
/// </summary>
public static string ToGeoJson(this OguLayer layer)
{
var features = new List<object>();
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
var geojsonGeom = GeometryUtil.Wkt2Geojson(feature.Wkt);
features.Add(new
{
type = "Feature",
geometry = JsonSerializer.Deserialize<object>(geojsonGeom),
properties = feature.Attributes.ToDictionary(
a => a.Key,
a => a.Value.Value)
});
}
var featureCollection = new
{
type = "FeatureCollection",
features
};
return JsonSerializer.Serialize(featureCollection, new JsonSerializerOptions
{
WriteIndented = true
});
}
/// <summary>
/// 合并图层
/// </summary>
public static OguLayer Merge(this OguLayer layer, OguLayer other)
{
if (layer.GeometryType != other.GeometryType)
{
throw new ArgumentException("几何类型不匹配");
}
var merged = layer.Clone();
// 添加缺失的字段
foreach (var field in other.Fields)
{
if (merged.GetField(field.Name) == null)
{
merged.AddField(field.Clone());
}
}
// 添加要素
int maxFid = merged.Features.Max(f => f.Fid);
foreach (var feature in other.Features)
{
var newFeature = feature.Clone();
newFeature.Fid = ++maxFid;
merged.AddFeature(newFeature);
}
return merged;
}
}
12.5 自定义工具类
12.5.1 几何工具扩展
public static class GeometryUtilExtensions
{
/// <summary>
/// 计算两点之间的距离(考虑地球曲率)
/// </summary>
public static double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
{
const double R = 6371000; // 地球半径(米)
double dLat = ToRadians(lat2 - lat1);
double dLon = ToRadians(lon2 - lon1);
double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
return R * c;
}
private static double ToRadians(double degrees) => degrees * Math.PI / 180;
/// <summary>
/// 生成规则网格
/// </summary>
public static OguLayer GenerateGrid(
double minX, double minY,
double maxX, double maxY,
double cellWidth, double cellHeight)
{
var layer = new OguLayer
{
Name = "Grid",
GeometryType = GeometryType.POLYGON,
Wkid = 4326
};
layer.AddField(new OguField { Name = "Row", DataType = FieldDataType.INTEGER });
layer.AddField(new OguField { Name = "Col", DataType = FieldDataType.INTEGER });
layer.AddField(new OguField { Name = "CellId", DataType = FieldDataType.STRING, Length = 20 });
int fid = 1;
int row = 0;
for (double y = minY; y < maxY; y += cellHeight)
{
int col = 0;
for (double x = minX; x < maxX; x += cellWidth)
{
double x1 = x, y1 = y;
double x2 = Math.Min(x + cellWidth, maxX);
double y2 = Math.Min(y + cellHeight, maxY);
var wkt = $"POLYGON (({x1} {y1}, {x2} {y1}, {x2} {y2}, {x1} {y2}, {x1} {y1}))";
var feature = new OguFeature
{
Fid = fid++,
Wkt = wkt
};
feature.SetValue("Row", row);
feature.SetValue("Col", col);
feature.SetValue("CellId", $"R{row:D3}C{col:D3}");
layer.AddFeature(feature);
col++;
}
row++;
}
return layer;
}
/// <summary>
/// 生成凸包
/// </summary>
public static string ConvexHullFromPoints(IEnumerable<(double x, double y)> points)
{
var pointsWkt = string.Join(", ", points.Select(p => $"({p.x} {p.y})"));
var multiPoint = $"MULTIPOINT ({pointsWkt})";
var geom = GeometryUtil.Wkt2Geometry(multiPoint);
var hull = GeometryUtil.ConvexHull(geom);
return GeometryUtil.Geometry2Wkt(hull);
}
}
12.5.2 投影工具扩展
public static class ProjectionUtil
{
/// <summary>
/// 批量坐标转换(优化版本)
/// </summary>
public static void TransformLayer(OguLayer layer, int sourceWkid, int targetWkid)
{
if (sourceWkid == targetWkid) return;
// 预创建转换器
GdalConfiguration.ConfigureGdal();
var sourceSrs = new SpatialReference(null);
sourceSrs.ImportFromEPSG(sourceWkid);
var targetSrs = new SpatialReference(null);
targetSrs.ImportFromEPSG(targetWkid);
var transform = new CoordinateTransformation(sourceSrs, targetSrs);
try
{
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
using var geom = OSGeo.OGR.Geometry.CreateFromWkt(feature.Wkt);
if (geom != null && geom.Transform(transform) == 0)
{
geom.ExportToWkt(out string wkt);
feature.Wkt = wkt;
}
}
layer.Wkid = targetWkid;
}
finally
{
transform.Dispose();
targetSrs.Dispose();
sourceSrs.Dispose();
}
}
/// <summary>
/// 自动检测最佳投影坐标系
/// </summary>
public static int GetBestProjectedCrs(OguLayer layer)
{
if (layer.Features.Count == 0)
return 4520; // 默认返回CGCS2000 39带
// 计算图层中心点
double sumX = 0, sumY = 0;
int count = 0;
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
var geom = GeometryUtil.Wkt2Geometry(feature.Wkt);
var centroid = GeometryUtil.Centroid(geom);
sumX += centroid.GetX(0);
sumY += centroid.GetY(0);
count++;
}
if (count == 0)
return 4520;
double centerX = sumX / count;
int zone = CrsUtil.GetDh(centerX);
return CrsUtil.GetProjectedWkid(zone);
}
}
12.6 最佳实践
12.6.1 扩展开发原则
- 开闭原则:对扩展开放,对修改关闭
- 单一职责:每个扩展类只负责一个功能
- 接口隔离:使用小而精的接口
- 依赖倒置:依赖抽象,不依赖具体实现
12.6.2 代码组织建议
MyGisExtensions/
├── Formats/ # 格式处理器
│ ├── CsvHandler.cs
│ └── GpxHandler.cs
├── Engines/ # 自定义引擎
│ └── NtsEngine.cs
├── Extensions/ # 扩展方法
│ ├── OguLayerExtensions.cs
│ └── GeometryUtilExtensions.cs
├── Utils/ # 工具类
│ ├── ProjectionUtil.cs
│ └── GridUtil.cs
└── Registry/ # 注册中心
├── FormatRegistry.cs
└── EngineRegistry.cs
12.6.3 测试建议
[TestClass]
public class ExtensionTests
{
[TestMethod]
public void CsvHandler_ReadWrite_RoundTrip()
{
// Arrange
var handler = new CsvHandler();
var testFile = "test_data.csv";
// Create test CSV
File.WriteAllLines(testFile, new[]
{
"name,lon,lat,value",
"Point1,116.404,39.915,100",
"Point2,121.473,31.230,200"
});
// Act
var layer = handler.Read(testFile);
// Assert
Assert.AreEqual(2, layer.GetFeatureCount());
Assert.AreEqual(GeometryType.POINT, layer.GeometryType);
// Cleanup
File.Delete(testFile);
}
[TestMethod]
public void OguLayerExtensions_GetBounds_ReturnsCorrectEnvelope()
{
// Arrange
var layer = new OguLayer { GeometryType = GeometryType.POINT };
layer.AddField(new OguField { Name = "Id", DataType = FieldDataType.INTEGER });
layer.AddFeature(new OguFeature { Fid = 1, Wkt = "POINT (0 0)" });
layer.AddFeature(new OguFeature { Fid = 2, Wkt = "POINT (10 10)" });
// Act
var bounds = layer.GetBounds();
// Assert
Assert.IsNotNull(bounds);
Assert.IsTrue(bounds.Contains("0 0"));
Assert.IsTrue(bounds.Contains("10 10"));
}
}
12.7 小结
本章介绍了OGU4Net的扩展开发:
- 添加新数据格式:实现IFormatHandler接口,注册格式处理器
- 添加新引擎:继承GisEngine,实现读写器接口
- 扩展模型:使用继承或扩展方法扩展OguLayer
- 自定义工具:创建几何、投影等工具扩展
- 最佳实践:遵循SOLID原则,合理组织代码
通过扩展开发,可以让OGU4Net适应更多应用场景,满足特定业务需求。

浙公网安备 33010602011771号