第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 扩展开发原则

  1. 开闭原则:对扩展开放,对修改关闭
  2. 单一职责:每个扩展类只负责一个功能
  3. 接口隔离:使用小而精的接口
  4. 依赖倒置:依赖抽象,不依赖具体实现

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的扩展开发:

  1. 添加新数据格式:实现IFormatHandler接口,注册格式处理器
  2. 添加新引擎:继承GisEngine,实现读写器接口
  3. 扩展模型:使用继承或扩展方法扩展OguLayer
  4. 自定义工具:创建几何、投影等工具扩展
  5. 最佳实践:遵循SOLID原则,合理组织代码

通过扩展开发,可以让OGU4Net适应更多应用场景,满足特定业务需求。

posted @ 2025-12-03 17:40  我才是银古  阅读(0)  评论(0)    收藏  举报