第09章 - C#/.NET绑定开发指南

第09章:C#/.NET绑定开发指南

9.1 .NET GDAL 简介

9.1.1 概述

GDAL 为 .NET 平台提供了完整的绑定支持,允许 C# 开发者在 Windows 和跨平台环境中使用 GDAL 的全部功能。.NET GDAL 绑定特别适合企业级 Windows 应用开发。

┌─────────────────────────────────────────────────────────────┐
│                    .NET GDAL 架构                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │                .NET 应用层                           │   │
│   │    WPF/WinForms │ ASP.NET │ 控制台应用              │   │
│   └─────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           ▼                                  │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              GDAL .NET 绑定                          │   │
│   │    OSGeo.GDAL │ OSGeo.OGR │ OSGeo.OSR              │   │
│   └─────────────────────────────────────────────────────┘   │
│                           │                                  │
│                           ▼ P/Invoke                         │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              GDAL 本地库 (C/C++)                     │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

9.1.2 可用的 NuGet 包

包名 描述 平台支持
MaxRev.Gdal.Universal 自动配置,跨平台支持 Windows, Linux, macOS
MaxRev.Gdal.WindowsRuntime.Minimal Windows 最小运行时 Windows
MaxRev.Gdal.LinuxRuntime.Minimal Linux 最小运行时 Linux
GDAL.Native 官方原生包 平台特定

9.2 环境配置

9.2.1 创建项目

# 创建新项目
dotnet new console -n GdalDemo -f net6.0
cd GdalDemo

# 添加 NuGet 包
dotnet add package MaxRev.Gdal.Universal

9.2.2 项目文件配置

<!-- GdalDemo.csproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MaxRev.Gdal.Universal" Version="3.7.0.100" />
  </ItemGroup>

</Project>

9.2.3 初始化 GDAL

using MaxRev.Gdal.Core;
using OSGeo.GDAL;
using OSGeo.OGR;
using OSGeo.OSR;

namespace GdalDemo;

/// <summary>
/// GDAL 初始化帮助类
/// </summary>
public static class GdalInitializer
{
    private static bool _initialized = false;
    private static readonly object _lock = new object();

    /// <summary>
    /// 初始化 GDAL
    /// </summary>
    public static void Initialize()
    {
        if (_initialized) return;

        lock (_lock)
        {
            if (_initialized) return;

            // 使用 MaxRev.Gdal 自动配置
            GdalBase.ConfigureAll();

            // 可选:设置配置选项
            Gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8", "YES");
            Gdal.SetConfigOption("SHAPE_ENCODING", "UTF-8");

            // 打印版本信息
            Console.WriteLine($"GDAL 版本: {Gdal.VersionInfo("VERSION_NUM")}");
            Console.WriteLine($"栅格驱动数: {Gdal.GetDriverCount()}");
            Console.WriteLine($"矢量驱动数: {Ogr.GetDriverCount()}");

            _initialized = true;
        }
    }

    /// <summary>
    /// 列出所有驱动
    /// </summary>
    public static void ListDrivers()
    {
        Initialize();

        Console.WriteLine("\n栅格驱动:");
        for (int i = 0; i < Gdal.GetDriverCount(); i++)
        {
            using var driver = Gdal.GetDriver(i);
            Console.WriteLine($"  {driver.ShortName}: {driver.LongName}");
        }

        Console.WriteLine("\n矢量驱动:");
        for (int i = 0; i < Ogr.GetDriverCount(); i++)
        {
            using var driver = Ogr.GetDriver(i);
            Console.WriteLine($"  {driver.GetName()}");
        }
    }
}

9.3 栅格数据处理

9.3.1 读取栅格数据

using OSGeo.GDAL;
using OSGeo.OSR;

namespace GdalDemo.Raster;

/// <summary>
/// 栅格数据读取器
/// </summary>
public class RasterReader
{
    /// <summary>
    /// 读取栅格数据信息
    /// </summary>
    public static void ReadRasterInfo(string filepath)
    {
        GdalInitializer.Initialize();

        using var ds = Gdal.Open(filepath, Access.GA_ReadOnly);

        if (ds == null)
        {
            Console.WriteLine($"无法打开文件: {filepath}");
            return;
        }

        // 基本信息
        Console.WriteLine($"文件: {ds.GetDescription()}");
        Console.WriteLine($"驱动: {ds.GetDriver().ShortName}");
        Console.WriteLine($"尺寸: {ds.RasterXSize} x {ds.RasterYSize}");
        Console.WriteLine($"波段数: {ds.RasterCount}");

        // 地理变换
        double[] geoTransform = new double[6];
        ds.GetGeoTransform(geoTransform);
        Console.WriteLine($"原点: ({geoTransform[0]}, {geoTransform[3]})");
        Console.WriteLine($"像素大小: ({geoTransform[1]}, {geoTransform[5]})");

        // 投影信息
        string projection = ds.GetProjection();
        if (!string.IsNullOrEmpty(projection))
        {
            using var srs = new SpatialReference(projection);
            Console.WriteLine($"坐标系: {srs.GetName()}");
        }

        // 波段信息
        for (int i = 1; i <= ds.RasterCount; i++)
        {
            using var band = ds.GetRasterBand(i);
            Console.WriteLine($"\n波段 {i}:");
            Console.WriteLine($"  数据类型: {Gdal.GetDataTypeName(band.DataType)}");

            double nodata;
            int hasNodata;
            band.GetNoDataValue(out nodata, out hasNodata);
            if (hasNodata != 0)
            {
                Console.WriteLine($"  无效值: {nodata}");
            }

            double min, max, mean, stddev;
            band.GetStatistics(0, 1, out min, out max, out mean, out stddev);
            Console.WriteLine($"  统计: min={min:F2}, max={max:F2}, mean={mean:F2}, std={stddev:F2}");
        }
    }

    /// <summary>
    /// 读取波段数据为数组
    /// </summary>
    public static double[,] ReadBandAsArray(string filepath, int bandIndex = 1)
    {
        GdalInitializer.Initialize();

        using var ds = Gdal.Open(filepath, Access.GA_ReadOnly);

        if (ds == null)
        {
            throw new Exception($"无法打开文件: {filepath}");
        }

        int width = ds.RasterXSize;
        int height = ds.RasterYSize;

        using var band = ds.GetRasterBand(bandIndex);

        // 读取为一维数组
        double[] data = new double[width * height];
        band.ReadRaster(0, 0, width, height, data, width, height, 0, 0);

        // 转换为二维数组
        double[,] result = new double[height, width];
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                result[y, x] = data[y * width + x];
            }
        }

        return result;
    }

    /// <summary>
    /// 分块读取栅格数据
    /// </summary>
    public static void ReadInBlocks(string filepath, Action<int, int, int, int, double[]> processor)
    {
        GdalInitializer.Initialize();

        using var ds = Gdal.Open(filepath, Access.GA_ReadOnly);

        if (ds == null)
        {
            throw new Exception($"无法打开文件: {filepath}");
        }

        int width = ds.RasterXSize;
        int height = ds.RasterYSize;

        using var band = ds.GetRasterBand(1);

        int blockWidth, blockHeight;
        band.GetBlockSize(out blockWidth, out blockHeight);

        Console.WriteLine($"块大小: {blockWidth} x {blockHeight}");

        for (int y = 0; y < height; y += blockHeight)
        {
            for (int x = 0; x < width; x += blockWidth)
            {
                int actualWidth = Math.Min(blockWidth, width - x);
                int actualHeight = Math.Min(blockHeight, height - y);

                double[] data = new double[actualWidth * actualHeight];
                band.ReadRaster(x, y, actualWidth, actualHeight, data, actualWidth, actualHeight, 0, 0);

                processor(x, y, actualWidth, actualHeight, data);
            }
        }
    }
}

9.3.2 创建栅格数据

using OSGeo.GDAL;
using OSGeo.OSR;

namespace GdalDemo.Raster;

/// <summary>
/// 栅格数据写入器
/// </summary>
public class RasterWriter
{
    /// <summary>
    /// 创建 GeoTIFF 文件
    /// </summary>
    public static void CreateGeoTiff(
        string filepath,
        int width,
        int height,
        int bands,
        DataType dataType,
        double[] geoTransform,
        int epsg,
        double[,]? data = null)
    {
        GdalInitializer.Initialize();

        using var driver = Gdal.GetDriverByName("GTiff");

        if (driver == null)
        {
            throw new Exception("GTiff 驱动不可用");
        }

        // 创建选项
        string[] options = new string[]
        {
            "COMPRESS=LZW",
            "TILED=YES",
            "BLOCKXSIZE=256",
            "BLOCKYSIZE=256"
        };

        using var ds = driver.Create(filepath, width, height, bands, dataType, options);

        if (ds == null)
        {
            throw new Exception($"无法创建文件: {filepath}");
        }

        // 设置地理变换
        if (geoTransform != null)
        {
            ds.SetGeoTransform(geoTransform);
        }

        // 设置投影
        if (epsg > 0)
        {
            using var srs = new SpatialReference("");
            srs.ImportFromEPSG(epsg);
            ds.SetProjection(srs.ExportToWkt());
        }

        // 写入数据
        if (data != null)
        {
            using var band = ds.GetRasterBand(1);

            // 转换为一维数组
            double[] flatData = new double[width * height];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    flatData[y * width + x] = data[y, x];
                }
            }

            band.WriteRaster(0, 0, width, height, flatData, width, height, 0, 0);
            band.SetNoDataValue(-9999);
            band.ComputeStatistics(false, out _, out _, out _, out _, null, null);
        }

        ds.FlushCache();
        Console.WriteLine($"创建完成: {filepath}");
    }

    /// <summary>
    /// 创建示例 DEM
    /// </summary>
    public static void CreateSampleDEM(string filepath)
    {
        int width = 1000;
        int height = 1000;

        // 生成模拟高程数据
        double[,] data = new double[height, width];
        Random rand = new Random();

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                data[y, x] = 100 +
                    50 * Math.Sin(x * Math.PI / 100) * Math.Cos(y * Math.PI / 100) +
                    rand.NextDouble() * 10;
            }
        }

        // 地理变换
        double[] geoTransform = new double[]
        {
            116.0,   // 原点X
            0.001,   // 像素宽度
            0,       // 旋转
            40.0,    // 原点Y
            0,       // 旋转
            -0.001   // 像素高度
        };

        CreateGeoTiff(filepath, width, height, 1, DataType.GDT_Float32,
                      geoTransform, 4326, data);
    }

    /// <summary>
    /// 创建 RGB 彩色影像
    /// </summary>
    public static void CreateRGBImage(
        string filepath,
        int width,
        int height,
        byte[,] red,
        byte[,] green,
        byte[,] blue,
        double[] geoTransform,
        int epsg)
    {
        GdalInitializer.Initialize();

        using var driver = Gdal.GetDriverByName("GTiff");

        string[] options = new string[]
        {
            "COMPRESS=LZW",
            "PHOTOMETRIC=RGB"
        };

        using var ds = driver.Create(filepath, width, height, 3, DataType.GDT_Byte, options);

        if (geoTransform != null)
        {
            ds.SetGeoTransform(geoTransform);
        }

        if (epsg > 0)
        {
            using var srs = new SpatialReference("");
            srs.ImportFromEPSG(epsg);
            ds.SetProjection(srs.ExportToWkt());
        }

        // 写入各波段
        byte[][] bandData = new byte[][] {
            FlattenArray(red),
            FlattenArray(green),
            FlattenArray(blue)
        };

        ColorInterp[] colorInterps = new ColorInterp[]
        {
            ColorInterp.GCI_RedBand,
            ColorInterp.GCI_GreenBand,
            ColorInterp.GCI_BlueBand
        };

        for (int i = 0; i < 3; i++)
        {
            using var band = ds.GetRasterBand(i + 1);
            band.WriteRaster(0, 0, width, height, bandData[i], width, height, 0, 0);
            band.SetColorInterpretation(colorInterps[i]);
        }

        ds.FlushCache();
    }

    private static byte[] FlattenArray(byte[,] array)
    {
        int height = array.GetLength(0);
        int width = array.GetLength(1);
        byte[] result = new byte[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                result[y * width + x] = array[y, x];
            }
        }

        return result;
    }
}

9.3.3 栅格处理操作

using OSGeo.GDAL;

namespace GdalDemo.Raster;

/// <summary>
/// 栅格处理操作
/// </summary>
public class RasterProcessor
{
    /// <summary>
    /// 重投影栅格
    /// </summary>
    public static void Reproject(string srcPath, string dstPath, string dstSRS)
    {
        GdalInitializer.Initialize();

        using var srcDs = Gdal.Open(srcPath, Access.GA_ReadOnly);

        if (srcDs == null)
        {
            throw new Exception($"无法打开文件: {srcPath}");
        }

        var options = new GDALWarpAppOptions(new string[]
        {
            "-t_srs", dstSRS,
            "-r", "bilinear",
            "-co", "COMPRESS=LZW",
            "-co", "TILED=YES"
        });

        using var dstDs = Gdal.Warp(dstPath, new Dataset[] { srcDs }, options, null, null);

        Console.WriteLine($"重投影完成: {dstPath}");
    }

    /// <summary>
    /// 裁剪栅格
    /// </summary>
    public static void Clip(string srcPath, string dstPath,
                           double minX, double minY, double maxX, double maxY)
    {
        GdalInitializer.Initialize();

        using var srcDs = Gdal.Open(srcPath, Access.GA_ReadOnly);

        var options = new GDALTranslateOptions(new string[]
        {
            "-projwin", minX.ToString(), maxY.ToString(), maxX.ToString(), minY.ToString(),
            "-co", "COMPRESS=LZW"
        });

        using var dstDs = Gdal.Translate(dstPath, srcDs, options, null, null);

        Console.WriteLine($"裁剪完成: {dstPath}");
    }

    /// <summary>
    /// 使用矢量边界裁剪
    /// </summary>
    public static void ClipByVector(string srcPath, string dstPath, string cutlinePath)
    {
        GdalInitializer.Initialize();

        using var srcDs = Gdal.Open(srcPath, Access.GA_ReadOnly);

        var options = new GDALWarpAppOptions(new string[]
        {
            "-cutline", cutlinePath,
            "-crop_to_cutline",
            "-dstnodata", "-9999",
            "-co", "COMPRESS=LZW"
        });

        using var dstDs = Gdal.Warp(dstPath, new Dataset[] { srcDs }, options, null, null);

        Console.WriteLine($"裁剪完成: {dstPath}");
    }

    /// <summary>
    /// 镶嵌多个栅格
    /// </summary>
    public static void Mosaic(string[] srcPaths, string dstPath)
    {
        GdalInitializer.Initialize();

        // 打开所有源数据集
        Dataset[] srcDatasets = new Dataset[srcPaths.Length];
        for (int i = 0; i < srcPaths.Length; i++)
        {
            srcDatasets[i] = Gdal.Open(srcPaths[i], Access.GA_ReadOnly);
            if (srcDatasets[i] == null)
            {
                throw new Exception($"无法打开文件: {srcPaths[i]}");
            }
        }

        try
        {
            // 创建 VRT
            var vrtOptions = new GDALBuildVRTOptions(Array.Empty<string>());
            using var vrtDs = Gdal.BuildVRT("", srcDatasets, vrtOptions, null, null);

            // 转换为 GeoTIFF
            var translateOptions = new GDALTranslateOptions(new string[]
            {
                "-co", "COMPRESS=LZW",
                "-co", "TILED=YES",
                "-co", "BIGTIFF=IF_SAFER"
            });

            using var dstDs = Gdal.Translate(dstPath, vrtDs, translateOptions, null, null);

            Console.WriteLine($"镶嵌完成: {dstPath}");
        }
        finally
        {
            foreach (var ds in srcDatasets)
            {
                ds?.Dispose();
            }
        }
    }

    /// <summary>
    /// 格式转换
    /// </summary>
    public static void Convert(string srcPath, string dstPath, string format)
    {
        GdalInitializer.Initialize();

        using var srcDs = Gdal.Open(srcPath, Access.GA_ReadOnly);

        var options = new GDALTranslateOptions(new string[]
        {
            "-of", format
        });

        using var dstDs = Gdal.Translate(dstPath, srcDs, options, null, null);

        Console.WriteLine($"转换完成: {dstPath}");
    }

    /// <summary>
    /// 创建金字塔
    /// </summary>
    public static void BuildOverviews(string filepath, int[] levels)
    {
        GdalInitializer.Initialize();

        using var ds = Gdal.Open(filepath, Access.GA_Update);

        if (ds == null)
        {
            throw new Exception($"无法打开文件: {filepath}");
        }

        if (levels == null || levels.Length == 0)
        {
            levels = new int[] { 2, 4, 8, 16, 32 };
        }

        ds.BuildOverviews("AVERAGE", levels, null, null);

        Console.WriteLine($"金字塔创建完成: {filepath}");
    }
}

9.4 矢量数据处理

9.4.1 读取矢量数据

using OSGeo.OGR;
using OSGeo.OSR;

namespace GdalDemo.Vector;

/// <summary>
/// 矢量数据读取器
/// </summary>
public class VectorReader
{
    /// <summary>
    /// 读取矢量数据信息
    /// </summary>
    public static void ReadVectorInfo(string filepath)
    {
        GdalInitializer.Initialize();

        using var ds = Ogr.Open(filepath, 0);

        if (ds == null)
        {
            Console.WriteLine($"无法打开文件: {filepath}");
            return;
        }

        Console.WriteLine($"数据源: {ds.GetDescription()}");
        Console.WriteLine($"图层数: {ds.GetLayerCount()}");

        for (int i = 0; i < ds.GetLayerCount(); i++)
        {
            using var layer = ds.GetLayerByIndex(i);

            Console.WriteLine($"\n图层 {i}: {layer.GetName()}");
            Console.WriteLine($"  要素数: {layer.GetFeatureCount(1)}");
            Console.WriteLine($"  几何类型: {Ogr.GeometryTypeToName(layer.GetGeomType())}");

            // 范围
            Envelope extent = new Envelope();
            layer.GetExtent(extent, 1);
            Console.WriteLine($"  范围: ({extent.MinX:F4}, {extent.MinY:F4}) - ({extent.MaxX:F4}, {extent.MaxY:F4})");

            // 空间参考
            using var srs = layer.GetSpatialRef();
            if (srs != null)
            {
                Console.WriteLine($"  坐标系: {srs.GetName()}");
            }

            // 字段定义
            using var layerDefn = layer.GetLayerDefn();
            Console.WriteLine($"  字段数: {layerDefn.GetFieldCount()}");

            for (int j = 0; j < layerDefn.GetFieldCount(); j++)
            {
                using var fieldDefn = layerDefn.GetFieldDefn(j);
                Console.WriteLine($"    - {fieldDefn.GetName()}: {fieldDefn.GetTypeName()}({fieldDefn.GetWidth()})");
            }
        }
    }

    /// <summary>
    /// 遍历要素
    /// </summary>
    public static void IterateFeatures(string filepath, Action<Feature>? processor = null)
    {
        GdalInitializer.Initialize();

        using var ds = Ogr.Open(filepath, 0);

        if (ds == null)
        {
            throw new Exception($"无法打开文件: {filepath}");
        }

        using var layer = ds.GetLayerByIndex(0);
        layer.ResetReading();

        Feature? feature;
        while ((feature = layer.GetNextFeature()) != null)
        {
            using (feature)
            {
                if (processor != null)
                {
                    processor(feature);
                }
                else
                {
                    Console.WriteLine($"FID: {feature.GetFID()}");

                    using var geom = feature.GetGeometryRef();
                    if (geom != null)
                    {
                        Console.WriteLine($"  几何类型: {geom.GetGeometryName()}");
                        Console.WriteLine($"  WKT: {geom.ExportToWkt().Substring(0, Math.Min(50, geom.ExportToWkt().Length))}...");
                    }
                }
            }
        }
    }

    /// <summary>
    /// 读取要素为字典列表
    /// </summary>
    public static List<Dictionary<string, object?>> ReadFeatures(
        string filepath,
        string? whereClause = null,
        (double MinX, double MinY, double MaxX, double MaxY)? bbox = null)
    {
        GdalInitializer.Initialize();

        using var ds = Ogr.Open(filepath, 0);

        if (ds == null)
        {
            throw new Exception($"无法打开文件: {filepath}");
        }

        using var layer = ds.GetLayerByIndex(0);

        // 设置过滤器
        if (!string.IsNullOrEmpty(whereClause))
        {
            layer.SetAttributeFilter(whereClause);
        }

        if (bbox.HasValue)
        {
            layer.SetSpatialFilterRect(bbox.Value.MinX, bbox.Value.MinY,
                                       bbox.Value.MaxX, bbox.Value.MaxY);
        }

        var features = new List<Dictionary<string, object?>>();

        using var layerDefn = layer.GetLayerDefn();

        Feature? feature;
        layer.ResetReading();

        while ((feature = layer.GetNextFeature()) != null)
        {
            using (feature)
            {
                var dict = new Dictionary<string, object?>
                {
                    ["fid"] = feature.GetFID()
                };

                using var geom = feature.GetGeometryRef();
                if (geom != null)
                {
                    dict["geometry"] = geom.ExportToWkt();
                }

                for (int i = 0; i < layerDefn.GetFieldCount(); i++)
                {
                    using var fieldDefn = layerDefn.GetFieldDefn(i);
                    string fieldName = fieldDefn.GetName();

                    if (feature.IsFieldNull(i))
                    {
                        dict[fieldName] = null;
                    }
                    else
                    {
                        dict[fieldName] = fieldDefn.GetFieldType() switch
                        {
                            FieldType.OFTInteger => feature.GetFieldAsInteger(i),
                            FieldType.OFTInteger64 => feature.GetFieldAsInteger64(i),
                            FieldType.OFTReal => feature.GetFieldAsDouble(i),
                            _ => feature.GetFieldAsString(i)
                        };
                    }
                }

                features.Add(dict);
            }
        }

        return features;
    }
}

9.4.2 创建矢量数据

using OSGeo.OGR;
using OSGeo.OSR;

namespace GdalDemo.Vector;

/// <summary>
/// 矢量数据写入器
/// </summary>
public class VectorWriter
{
    /// <summary>
    /// 创建 Shapefile
    /// </summary>
    public static void CreateShapefile(
        string filepath,
        wkbGeometryType geomType,
        int epsg,
        List<(string Name, FieldType Type, int Width)> fields,
        List<Dictionary<string, object?>> features)
    {
        GdalInitializer.Initialize();

        using var driver = Ogr.GetDriverByName("ESRI Shapefile");

        if (driver == null)
        {
            throw new Exception("Shapefile 驱动不可用");
        }

        // 删除已存在的文件
        if (File.Exists(filepath))
        {
            driver.DeleteDataSource(filepath);
        }

        using var ds = driver.CreateDataSource(filepath, Array.Empty<string>());

        if (ds == null)
        {
            throw new Exception($"无法创建数据源: {filepath}");
        }

        // 创建空间参考
        using var srs = new SpatialReference("");
        srs.ImportFromEPSG(epsg);

        // 创建图层
        using var layer = ds.CreateLayer("layer", srs, geomType, Array.Empty<string>());

        // 创建字段
        foreach (var (name, type, width) in fields)
        {
            using var fieldDefn = new FieldDefn(name, type);
            if (width > 0)
            {
                fieldDefn.SetWidth(width);
            }
            layer.CreateField(fieldDefn, 1);
        }

        // 获取图层定义
        using var layerDefn = layer.GetLayerDefn();

        // 添加要素
        foreach (var featureData in features)
        {
            using var feature = new Feature(layerDefn);

            // 设置几何
            if (featureData.TryGetValue("geometry", out var geomObj) && geomObj is string wkt)
            {
                using var geom = Geometry.CreateFromWkt(wkt);
                feature.SetGeometry(geom);
            }

            // 设置属性
            foreach (var (name, _, _) in fields)
            {
                if (featureData.TryGetValue(name, out var value) && value != null)
                {
                    int fieldIndex = feature.GetFieldIndex(name);
                    if (fieldIndex >= 0)
                    {
                        switch (value)
                        {
                            case int intVal:
                                feature.SetField(fieldIndex, intVal);
                                break;
                            case long longVal:
                                feature.SetField(fieldIndex, longVal);
                                break;
                            case double doubleVal:
                                feature.SetField(fieldIndex, doubleVal);
                                break;
                            default:
                                feature.SetField(fieldIndex, value.ToString());
                                break;
                        }
                    }
                }
            }

            layer.CreateFeature(feature);
        }

        Console.WriteLine($"创建完成: {filepath}");
    }

    /// <summary>
    /// 创建 GeoJSON
    /// </summary>
    public static void CreateGeoJSON(
        string filepath,
        wkbGeometryType geomType,
        int epsg,
        List<(string Name, FieldType Type, int Width)> fields,
        List<Dictionary<string, object?>> features)
    {
        GdalInitializer.Initialize();

        using var driver = Ogr.GetDriverByName("GeoJSON");

        if (driver == null)
        {
            throw new Exception("GeoJSON 驱动不可用");
        }

        using var ds = driver.CreateDataSource(filepath, Array.Empty<string>());

        using var srs = new SpatialReference("");
        srs.ImportFromEPSG(epsg);

        using var layer = ds.CreateLayer("features", srs, geomType, Array.Empty<string>());

        foreach (var (name, type, width) in fields)
        {
            using var fieldDefn = new FieldDefn(name, type);
            if (width > 0)
            {
                fieldDefn.SetWidth(width);
            }
            layer.CreateField(fieldDefn, 1);
        }

        using var layerDefn = layer.GetLayerDefn();

        foreach (var featureData in features)
        {
            using var feature = new Feature(layerDefn);

            if (featureData.TryGetValue("geometry", out var geomObj) && geomObj is string wkt)
            {
                using var geom = Geometry.CreateFromWkt(wkt);
                feature.SetGeometry(geom);
            }

            foreach (var (name, _, _) in fields)
            {
                if (featureData.TryGetValue(name, out var value) && value != null)
                {
                    int fieldIndex = feature.GetFieldIndex(name);
                    if (fieldIndex >= 0)
                    {
                        feature.SetField(fieldIndex, value.ToString());
                    }
                }
            }

            layer.CreateFeature(feature);
        }

        Console.WriteLine($"创建完成: {filepath}");
    }

    /// <summary>
    /// 创建示例点数据
    /// </summary>
    public static void CreateSamplePoints(string filepath)
    {
        var fields = new List<(string Name, FieldType Type, int Width)>
        {
            ("name", FieldType.OFTString, 50),
            ("population", FieldType.OFTInteger64, 0)
        };

        var features = new List<Dictionary<string, object?>>
        {
            new() { ["geometry"] = "POINT(116.4 39.9)", ["name"] = "北京", ["population"] = 21540000L },
            new() { ["geometry"] = "POINT(121.5 31.2)", ["name"] = "上海", ["population"] = 24280000L },
            new() { ["geometry"] = "POINT(113.3 23.1)", ["name"] = "广州", ["population"] = 18680000L }
        };

        CreateShapefile(filepath, wkbGeometryType.wkbPoint, 4326, fields, features);
    }
}

9.5 坐标转换

using OSGeo.OGR;
using OSGeo.OSR;

namespace GdalDemo.Coordinate;

/// <summary>
/// 坐标转换工具
/// </summary>
public class CoordinateTransformer
{
    /// <summary>
    /// 转换坐标点
    /// </summary>
    public static (double X, double Y) TransformPoint(
        double x, double y,
        int srcEPSG, int dstEPSG)
    {
        GdalInitializer.Initialize();

        using var srcSRS = new SpatialReference("");
        srcSRS.ImportFromEPSG(srcEPSG);

        using var dstSRS = new SpatialReference("");
        dstSRS.ImportFromEPSG(dstEPSG);

        using var transform = new CoordinateTransformation(srcSRS, dstSRS);

        double[] point = new double[] { x, y, 0 };
        transform.TransformPoint(point);

        return (point[0], point[1]);
    }

    /// <summary>
    /// 批量转换坐标
    /// </summary>
    public static List<(double X, double Y)> TransformPoints(
        List<(double X, double Y)> points,
        int srcEPSG, int dstEPSG)
    {
        GdalInitializer.Initialize();

        using var srcSRS = new SpatialReference("");
        srcSRS.ImportFromEPSG(srcEPSG);

        using var dstSRS = new SpatialReference("");
        dstSRS.ImportFromEPSG(dstEPSG);

        using var transform = new CoordinateTransformation(srcSRS, dstSRS);

        var results = new List<(double X, double Y)>();

        foreach (var (x, y) in points)
        {
            double[] point = new double[] { x, y, 0 };
            transform.TransformPoint(point);
            results.Add((point[0], point[1]));
        }

        return results;
    }

    /// <summary>
    /// 转换几何对象
    /// </summary>
    public static Geometry TransformGeometry(
        Geometry geom,
        int srcEPSG, int dstEPSG)
    {
        GdalInitializer.Initialize();

        using var srcSRS = new SpatialReference("");
        srcSRS.ImportFromEPSG(srcEPSG);

        using var dstSRS = new SpatialReference("");
        dstSRS.ImportFromEPSG(dstEPSG);

        using var transform = new CoordinateTransformation(srcSRS, dstSRS);

        var newGeom = geom.Clone();
        newGeom.Transform(transform);

        return newGeom;
    }
}

9.6 实用工具类

using OSGeo.GDAL;
using OSGeo.OGR;

namespace GdalDemo.Utils;

/// <summary>
/// GDAL 资源包装器
/// </summary>
public class GdalResource : IDisposable
{
    private Dataset? _rasterDs;
    private DataSource? _vectorDs;
    private bool _disposed = false;

    public Dataset? RasterDataset => _rasterDs;
    public DataSource? VectorDataSource => _vectorDs;

    private GdalResource() { }

    public static GdalResource OpenRaster(string filepath, Access access = Access.GA_ReadOnly)
    {
        GdalInitializer.Initialize();

        var resource = new GdalResource();
        resource._rasterDs = Gdal.Open(filepath, access);

        if (resource._rasterDs == null)
        {
            throw new Exception($"无法打开栅格文件: {filepath}");
        }

        return resource;
    }

    public static GdalResource OpenVector(string filepath, int update = 0)
    {
        GdalInitializer.Initialize();

        var resource = new GdalResource();
        resource._vectorDs = Ogr.Open(filepath, update);

        if (resource._vectorDs == null)
        {
            throw new Exception($"无法打开矢量文件: {filepath}");
        }

        return resource;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _rasterDs?.Dispose();
                _vectorDs?.Dispose();
            }

            _rasterDs = null;
            _vectorDs = null;
            _disposed = true;
        }
    }

    ~GdalResource()
    {
        Dispose(false);
    }
}

9.7 主程序示例

using GdalDemo;
using GdalDemo.Raster;
using GdalDemo.Vector;
using GdalDemo.Coordinate;

// 初始化 GDAL
GdalInitializer.Initialize();

Console.WriteLine("=== GDAL .NET 示例 ===\n");

// 1. 创建示例 DEM
Console.WriteLine("1. 创建示例 DEM...");
RasterWriter.CreateSampleDEM("sample_dem.tif");

// 2. 读取栅格信息
Console.WriteLine("\n2. 读取栅格信息...");
RasterReader.ReadRasterInfo("sample_dem.tif");

// 3. 创建示例点数据
Console.WriteLine("\n3. 创建示例点数据...");
VectorWriter.CreateSamplePoints("cities.shp");

// 4. 读取矢量信息
Console.WriteLine("\n4. 读取矢量信息...");
VectorReader.ReadVectorInfo("cities.shp");

// 5. 坐标转换
Console.WriteLine("\n5. 坐标转换...");
var (x, y) = CoordinateTransformer.TransformPoint(116.4, 39.9, 4326, 3857);
Console.WriteLine($"WGS84 (116.4, 39.9) -> Web Mercator ({x:F2}, {y:F2})");

Console.WriteLine("\n=== 完成 ===");

9.8 本章小结

本章介绍了 C#/.NET GDAL 开发:

  1. 环境配置:使用 MaxRev.Gdal.Universal NuGet 包
  2. 栅格处理:读取、创建和处理栅格数据
  3. 矢量处理:读取、创建和查询矢量数据
  4. 坐标转换:使用 OSR 进行坐标转换
  5. 最佳实践:资源管理和 IDisposable 模式

9.9 思考与练习

  1. 比较 MaxRev.Gdal.Universal 和其他 GDAL NuGet 包的优缺点。
  2. 如何在 ASP.NET Core Web API 中使用 GDAL?
  3. 实现一个支持异步操作的栅格处理类。
  4. 编写一个批量格式转换工具。
  5. 如何处理 GDAL 中的大文件和内存管理?
posted @ 2025-12-29 12:40  我才是银古  阅读(13)  评论(0)    收藏  举报