第11章 - 开发实战案例
第11章 - 开发实战案例
11.1 案例概述
本章通过实际案例展示OGU4Net的综合应用,涵盖:
- 数据格式批量转换工具
- 空间数据查询服务
- 国土数据处理系统
- 面积计算与统计工具
- 数据质量检查工具
11.2 案例一:数据格式批量转换工具
11.2.1 需求描述
开发一个命令行工具,支持:
- 批量转换Shapefile到GeoJSON
- 自动处理编码问题
- 可选的坐标转换
- 进度显示和错误报告
11.2.2 实现代码
using OpenGIS.Utils.DataSource;
using OpenGIS.Utils.Engine.Enums;
using OpenGIS.Utils.Engine.Util;
using OpenGIS.Utils.Utils;
namespace GisConverter;
public class BatchConverter
{
private readonly ConvertOptions _options;
private int _successCount;
private int _failCount;
private readonly List<string> _errors = new();
public BatchConverter(ConvertOptions options)
{
_options = options;
}
public void Convert()
{
// 验证输入目录
if (!Directory.Exists(_options.InputDir))
{
Console.Error.WriteLine($"输入目录不存在: {_options.InputDir}");
return;
}
// 创建输出目录
Directory.CreateDirectory(_options.OutputDir);
// 获取文件列表
var files = Directory.GetFiles(_options.InputDir, "*.shp");
var sortedFiles = SortUtil.NaturalSort(files, Path.GetFileName);
Console.WriteLine($"找到 {files.Length} 个Shapefile");
Console.WriteLine($"输出格式: {_options.OutputFormat}");
if (_options.TargetWkid.HasValue)
{
Console.WriteLine($"目标坐标系: EPSG:{_options.TargetWkid}");
}
Console.WriteLine();
// 处理每个文件
int index = 0;
foreach (var file in sortedFiles)
{
index++;
var fileName = Path.GetFileName(file);
Console.Write($"[{index}/{files.Length}] {fileName}... ");
try
{
ConvertFile(file);
_successCount++;
Console.WriteLine("成功");
}
catch (Exception ex)
{
_failCount++;
_errors.Add($"{fileName}: {ex.Message}");
Console.WriteLine($"失败 - {ex.Message}");
}
}
// 输出统计
Console.WriteLine();
Console.WriteLine($"转换完成: 成功 {_successCount}, 失败 {_failCount}");
if (_errors.Any())
{
Console.WriteLine("\n错误详情:");
foreach (var error in _errors)
{
Console.WriteLine($" - {error}");
}
}
}
private void ConvertFile(string inputPath)
{
// 读取Shapefile
var encoding = ShpUtil.GetShapefileEncoding(inputPath);
var layer = OguLayerUtil.ReadLayer(DataFormatType.SHP, inputPath);
// 坐标转换
if (_options.TargetWkid.HasValue && layer.Wkid.HasValue)
{
foreach (var feature in layer.Features)
{
if (!string.IsNullOrEmpty(feature.Wkt))
{
feature.Wkt = CrsUtil.Transform(
feature.Wkt,
layer.Wkid.Value,
_options.TargetWkid.Value);
}
}
layer.Wkid = _options.TargetWkid.Value;
}
// 确定输出路径和格式
var baseName = Path.GetFileNameWithoutExtension(inputPath);
string outputPath;
DataFormatType outputFormat;
switch (_options.OutputFormat.ToLower())
{
case "geojson":
outputPath = Path.Combine(_options.OutputDir, baseName + ".geojson");
outputFormat = DataFormatType.GEOJSON;
break;
case "gpkg":
outputPath = Path.Combine(_options.OutputDir, baseName + ".gpkg");
outputFormat = DataFormatType.GEOPACKAGE;
break;
case "kml":
outputPath = Path.Combine(_options.OutputDir, baseName + ".kml");
outputFormat = DataFormatType.KML;
break;
default:
throw new ArgumentException($"不支持的输出格式: {_options.OutputFormat}");
}
// 写入
OguLayerUtil.WriteLayer(outputFormat, layer, outputPath);
}
}
public class ConvertOptions
{
public string InputDir { get; set; } = "";
public string OutputDir { get; set; } = "";
public string OutputFormat { get; set; } = "geojson";
public int? TargetWkid { get; set; }
}
// 程序入口
class Program
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("用法: GisConverter <输入目录> <输出目录> [选项]");
Console.WriteLine("选项:");
Console.WriteLine(" -f <格式> 输出格式 (geojson, gpkg, kml)");
Console.WriteLine(" -t <WKID> 目标坐标系EPSG代码");
return;
}
var options = new ConvertOptions
{
InputDir = args[0],
OutputDir = args[1]
};
// 解析可选参数
for (int i = 2; i < args.Length; i++)
{
if (args[i] == "-f" && i + 1 < args.Length)
{
options.OutputFormat = args[++i];
}
else if (args[i] == "-t" && i + 1 < args.Length)
{
options.TargetWkid = int.Parse(args[++i]);
}
}
var converter = new BatchConverter(options);
converter.Convert();
}
}
11.3 案例二:空间数据查询服务
11.3.1 需求描述
开发一个REST API服务,提供:
- 点查询(查询点所在的区域)
- 范围查询(查询指定范围内的要素)
- 缓冲区查询
11.3.2 实现代码
using Microsoft.AspNetCore.Mvc;
using OpenGIS.Utils.DataSource;
using OpenGIS.Utils.Engine.Enums;
using OpenGIS.Utils.Geometry;
namespace GisQueryService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SpatialQueryController : ControllerBase
{
private readonly string _dataPath;
public SpatialQueryController(IConfiguration config)
{
_dataPath = config["DataPath"] ?? "data/";
}
/// <summary>
/// 点查询 - 查询点所在的区域
/// </summary>
[HttpGet("point")]
public IActionResult PointQuery(
[FromQuery] double x,
[FromQuery] double y,
[FromQuery] string layer)
{
try
{
var layerPath = Path.Combine(_dataPath, layer + ".shp");
if (!System.IO.File.Exists(layerPath))
{
return NotFound(new { error = $"图层不存在: {layer}" });
}
var pointWkt = $"POINT ({x} {y})";
var data = OguLayerUtil.ReadLayer(DataFormatType.SHP, layerPath);
var results = new List<object>();
foreach (var feature in data.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
if (GeometryUtil.ContainsWkt(feature.Wkt, pointWkt))
{
results.Add(new
{
fid = feature.Fid,
attributes = feature.Attributes.ToDictionary(
a => a.Key,
a => a.Value.Value)
});
}
}
return Ok(new
{
query = new { x, y },
count = results.Count,
features = results
});
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
/// <summary>
/// 范围查询 - 查询矩形范围内的要素
/// </summary>
[HttpGet("bbox")]
public IActionResult BboxQuery(
[FromQuery] double minX,
[FromQuery] double minY,
[FromQuery] double maxX,
[FromQuery] double maxY,
[FromQuery] string layer)
{
try
{
var layerPath = Path.Combine(_dataPath, layer + ".shp");
if (!System.IO.File.Exists(layerPath))
{
return NotFound(new { error = $"图层不存在: {layer}" });
}
var bboxWkt = $"POLYGON (({minX} {minY}, {maxX} {minY}, {maxX} {maxY}, {minX} {maxY}, {minX} {minY}))";
// 使用空间过滤读取
var data = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
layerPath,
spatialFilterWkt: bboxWkt);
var features = data.Features.Select(f => new
{
fid = f.Fid,
geometry = f.Wkt,
attributes = f.Attributes.ToDictionary(
a => a.Key,
a => a.Value.Value)
});
return Ok(new
{
query = new { minX, minY, maxX, maxY },
count = data.GetFeatureCount(),
features
});
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
/// <summary>
/// 缓冲区查询 - 查询指定距离内的要素
/// </summary>
[HttpGet("buffer")]
public IActionResult BufferQuery(
[FromQuery] double x,
[FromQuery] double y,
[FromQuery] double distance,
[FromQuery] string layer)
{
try
{
var layerPath = Path.Combine(_dataPath, layer + ".shp");
if (!System.IO.File.Exists(layerPath))
{
return NotFound(new { error = $"图层不存在: {layer}" });
}
var pointWkt = $"POINT ({x} {y})";
var bufferWkt = GeometryUtil.BufferWkt(pointWkt, distance);
var data = OguLayerUtil.ReadLayer(
DataFormatType.SHP,
layerPath,
spatialFilterWkt: bufferWkt);
var features = data.Features.Select(f => new
{
fid = f.Fid,
geometry = f.Wkt,
attributes = f.Attributes.ToDictionary(
a => a.Key,
a => a.Value.Value)
});
return Ok(new
{
query = new { x, y, distance },
bufferGeometry = bufferWkt,
count = data.GetFeatureCount(),
features
});
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
/// <summary>
/// 几何转换接口
/// </summary>
[HttpPost("geometry/convert")]
public IActionResult ConvertGeometry([FromBody] GeometryConvertRequest request)
{
try
{
string result = request.TargetFormat.ToLower() switch
{
"geojson" => GeometryUtil.Wkt2Geojson(request.Wkt),
"wkt" => request.Wkt, // 已经是WKT
_ => throw new ArgumentException($"不支持的目标格式: {request.TargetFormat}")
};
return Ok(new { result });
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
public class GeometryConvertRequest
{
public string Wkt { get; set; } = "";
public string TargetFormat { get; set; } = "geojson";
}
11.4 案例三:国土数据处理系统
11.4.1 需求描述
处理国土部门的TXT坐标文件:
- 读取TXT坐标文件
- 构建宗地多边形
- 计算面积
- 输出为Shapefile
11.4.2 实现代码
using OpenGIS.Utils.DataSource;
using OpenGIS.Utils.Engine.Enums;
using OpenGIS.Utils.Engine.Model.Layer;
using OpenGIS.Utils.Engine.Util;
using OpenGIS.Utils.Geometry;
namespace LandProcessor;
public class ParcelProcessor
{
/// <summary>
/// 处理TXT坐标文件,生成宗地Shapefile
/// </summary>
public OguLayer ProcessTxtToParcel(string txtPath, int targetWkid = 4490)
{
// 1. 读取TXT文件
Console.WriteLine($"读取TXT文件: {txtPath}");
var txtLayer = GtTxtUtil.LoadTxt(txtPath);
Console.WriteLine($"元数据: {txtLayer.Metadata?.CoordinateSystemName}");
Console.WriteLine($"读取到 {txtLayer.GetFeatureCount()} 个坐标点");
// 2. 按圈号分组
var featuresByRing = txtLayer.Features
.GroupBy(f => f.GetValue("圈号")?.ToString() ?? "1")
.OrderBy(g => g.Key)
.ToList();
Console.WriteLine($"共 {featuresByRing.Count} 个环");
// 3. 创建多边形图层
var parcelLayer = new OguLayer
{
Name = Path.GetFileNameWithoutExtension(txtPath),
GeometryType = GeometryType.POLYGON,
Wkid = targetWkid
};
// 添加字段
parcelLayer.AddField(new OguField { Name = "Name", DataType = FieldDataType.STRING, Length = 100 });
parcelLayer.AddField(new OguField { Name = "Area", DataType = FieldDataType.DOUBLE });
parcelLayer.AddField(new OguField { Name = "Perimeter", DataType = FieldDataType.DOUBLE });
parcelLayer.AddField(new OguField { Name = "PointCount", DataType = FieldDataType.INTEGER });
// 4. 构建多边形
var rings = new List<string>();
foreach (var group in featuresByRing)
{
var points = group
.OrderBy(f => f.GetValue("点号")?.ToString())
.Select(f =>
{
var x = f.GetAttribute("X")?.GetDoubleValue() ?? 0;
var y = f.GetAttribute("Y")?.GetDoubleValue() ?? 0;
return $"{x} {y}";
})
.ToList();
if (points.Count < 3)
{
Console.WriteLine($"警告: 环 {group.Key} 点数不足,跳过");
continue;
}
// 闭合环
if (points.First() != points.Last())
{
points.Add(points.First());
}
rings.Add($"({string.Join(", ", points)})");
}
if (rings.Count == 0)
{
throw new Exception("没有有效的环");
}
// 构建多边形WKT
string wkt;
if (rings.Count == 1)
{
wkt = $"POLYGON ({rings[0]})";
}
else
{
// 第一个环是外环,其余是内环(孔洞)
wkt = $"POLYGON ({string.Join(", ", rings)})";
}
Console.WriteLine("验证几何...");
var geom = GeometryUtil.Wkt2Geometry(wkt);
var validResult = GeometryUtil.IsValid(geom);
if (!validResult.IsValid)
{
Console.WriteLine($"警告: 几何无效 - {validResult.ErrorMessage}");
}
// 5. 计算属性
// 先转到投影坐标系计算面积
int zone = CrsUtil.GetDh(geom);
int projWkid = CrsUtil.GetProjectedWkid(zone);
string projWkt = CrsUtil.Transform(wkt, targetWkid, projWkid);
double area = GeometryUtil.AreaWkt(projWkt);
double perimeter = GeometryUtil.LengthWkt(projWkt);
int pointCount = txtLayer.GetFeatureCount();
// 6. 创建要素
var feature = new OguFeature
{
Fid = 1,
Wkt = wkt
};
feature.SetValue("Name", parcelLayer.Name);
feature.SetValue("Area", area);
feature.SetValue("Perimeter", perimeter);
feature.SetValue("PointCount", pointCount);
parcelLayer.AddFeature(feature);
Console.WriteLine($"面积: {area / 10000:F4} 公顷");
Console.WriteLine($"周长: {perimeter:F2} 米");
return parcelLayer;
}
/// <summary>
/// 批量处理TXT文件
/// </summary>
public void BatchProcess(string inputDir, string outputDir)
{
Directory.CreateDirectory(outputDir);
var txtFiles = Directory.GetFiles(inputDir, "*.txt");
Console.WriteLine($"找到 {txtFiles.Length} 个TXT文件\n");
var allParcels = new OguLayer
{
Name = "AllParcels",
GeometryType = GeometryType.POLYGON,
Wkid = 4490
};
allParcels.AddField(new OguField { Name = "Name", DataType = FieldDataType.STRING, Length = 100 });
allParcels.AddField(new OguField { Name = "Area", DataType = FieldDataType.DOUBLE });
allParcels.AddField(new OguField { Name = "Perimeter", DataType = FieldDataType.DOUBLE });
allParcels.AddField(new OguField { Name = "SourceFile", DataType = FieldDataType.STRING, Length = 200 });
int fid = 1;
double totalArea = 0;
foreach (var txtFile in txtFiles)
{
Console.WriteLine($"========== {Path.GetFileName(txtFile)} ==========");
try
{
var parcel = ProcessTxtToParcel(txtFile);
var feature = parcel.Features.First();
var newFeature = new OguFeature
{
Fid = fid++,
Wkt = feature.Wkt
};
newFeature.SetValue("Name", feature.GetValue("Name"));
newFeature.SetValue("Area", feature.GetValue("Area"));
newFeature.SetValue("Perimeter", feature.GetValue("Perimeter"));
newFeature.SetValue("SourceFile", Path.GetFileName(txtFile));
allParcels.AddFeature(newFeature);
var area = feature.GetAttribute("Area")?.GetDoubleValue() ?? 0;
totalArea += area;
}
catch (Exception ex)
{
Console.Error.WriteLine($"处理失败: {ex.Message}");
}
Console.WriteLine();
}
// 保存合并结果
var outputPath = Path.Combine(outputDir, "all_parcels.shp");
OguLayerUtil.WriteLayer(DataFormatType.SHP, allParcels, outputPath);
Console.WriteLine($"\n======== 汇总 ========");
Console.WriteLine($"成功处理: {allParcels.GetFeatureCount()} 个宗地");
Console.WriteLine($"总面积: {totalArea / 10000:F4} 公顷");
Console.WriteLine($"输出文件: {outputPath}");
}
}
11.5 案例四:面积计算与统计工具
11.5.1 实现代码
using OpenGIS.Utils.DataSource;
using OpenGIS.Utils.Engine.Enums;
using OpenGIS.Utils.Engine.Model.Layer;
using OpenGIS.Utils.Engine.Util;
using OpenGIS.Utils.Geometry;
namespace AreaCalculator;
public class AreaStatistics
{
/// <summary>
/// 计算图层面积统计
/// </summary>
public AreaReport Calculate(string shpPath, string groupField = null)
{
var layer = OguLayerUtil.ReadLayer(DataFormatType.SHP, shpPath);
var report = new AreaReport
{
LayerName = layer.Name,
FeatureCount = layer.GetFeatureCount(),
GroupField = groupField
};
foreach (var feature in layer.Features)
{
if (string.IsNullOrEmpty(feature.Wkt)) continue;
// 计算面积
double area = CalculateAreaInSquareMeters(feature.Wkt, layer.Wkid ?? 4326);
report.TotalArea += area;
// 分组统计
if (!string.IsNullOrEmpty(groupField))
{
var groupValue = feature.GetValue(groupField)?.ToString() ?? "未分类";
if (!report.GroupAreas.ContainsKey(groupValue))
{
report.GroupAreas[groupValue] = 0;
}
report.GroupAreas[groupValue] += area;
}
}
return report;
}
private double CalculateAreaInSquareMeters(string wkt, int wkid)
{
if (CrsUtil.IsProjectedCRS(wkid))
{
// 已经是投影坐标系
return GeometryUtil.AreaWkt(wkt);
}
else
{
// 转换到投影坐标系
var geom = GeometryUtil.Wkt2Geometry(wkt);
int zone = CrsUtil.GetDh(geom);
int projWkid = CrsUtil.GetProjectedWkid(zone);
string projWkt = CrsUtil.Transform(wkt, wkid, projWkid);
return GeometryUtil.AreaWkt(projWkt);
}
}
/// <summary>
/// 输出报告
/// </summary>
public void PrintReport(AreaReport report)
{
Console.WriteLine("=".PadRight(50, '='));
Console.WriteLine($"图层名称: {report.LayerName}");
Console.WriteLine($"要素数量: {report.FeatureCount}");
Console.WriteLine($"总面积: {report.TotalArea / 10000:N4} 公顷");
Console.WriteLine($"总面积: {report.TotalArea / 1000000:N6} 平方公里");
if (report.GroupAreas.Any())
{
Console.WriteLine();
Console.WriteLine($"按 {report.GroupField} 分组统计:");
Console.WriteLine("-".PadRight(40, '-'));
foreach (var (group, area) in report.GroupAreas.OrderByDescending(x => x.Value))
{
var percent = area / report.TotalArea * 100;
Console.WriteLine($" {group}: {area / 10000:N4} 公顷 ({percent:F1}%)");
}
}
Console.WriteLine("=".PadRight(50, '='));
}
}
public class AreaReport
{
public string LayerName { get; set; }
public int FeatureCount { get; set; }
public double TotalArea { get; set; }
public string GroupField { get; set; }
public Dictionary<string, double> GroupAreas { get; set; } = new();
}
11.6 案例五:数据质量检查工具
11.6.1 实现代码
using OpenGIS.Utils.DataSource;
using OpenGIS.Utils.Engine.Enums;
using OpenGIS.Utils.Geometry;
using OpenGIS.Utils.Engine.Model.Layer;
namespace DataQualityChecker;
public class QualityChecker
{
public QualityReport Check(string shpPath)
{
var layer = OguLayerUtil.ReadLayer(DataFormatType.SHP, shpPath);
var report = new QualityReport
{
LayerName = layer.Name,
FeatureCount = layer.GetFeatureCount(),
FieldCount = layer.Fields.Count
};
// 检查每个要素
foreach (var feature in layer.Features)
{
// 1. 空几何检查
if (string.IsNullOrEmpty(feature.Wkt))
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "空几何",
Description = "要素几何为空",
Severity = "错误"
});
continue;
}
try
{
var geom = GeometryUtil.Wkt2Geometry(feature.Wkt);
// 2. 有效性检查
var validResult = GeometryUtil.IsValid(geom);
if (!validResult.IsValid)
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "几何无效",
Description = validResult.ErrorMessage ?? "几何不满足OGC规范",
Severity = "错误"
});
}
// 3. 简单性检查
var simpleResult = GeometryUtil.IsSimple(geom);
if (!simpleResult.IsSimple)
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "几何不简单",
Description = simpleResult.Reason ?? "几何存在自相交",
Severity = "警告"
});
}
// 4. 小面积检查(面要素)
if (layer.GeometryType == GeometryType.POLYGON ||
layer.GeometryType == GeometryType.MULTIPOLYGON)
{
double area = GeometryUtil.Area(geom);
if (area < 0.000001)
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "微小面积",
Description = $"面积过小: {area}",
Severity = "警告"
});
}
}
// 5. 空值属性检查
foreach (var field in layer.Fields)
{
if (!field.IsNullable)
{
var value = feature.GetValue(field.Name);
if (value == null || (value is string s && string.IsNullOrWhiteSpace(s)))
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "空值字段",
Description = $"非空字段 {field.Name} 的值为空",
Severity = "错误"
});
}
}
}
}
catch (Exception ex)
{
report.Issues.Add(new QualityIssue
{
Fid = feature.Fid,
IssueType = "解析错误",
Description = ex.Message,
Severity = "错误"
});
}
}
// 统计
report.ErrorCount = report.Issues.Count(i => i.Severity == "错误");
report.WarningCount = report.Issues.Count(i => i.Severity == "警告");
return report;
}
public void PrintReport(QualityReport report)
{
Console.WriteLine("=".PadRight(60, '='));
Console.WriteLine("数据质量检查报告");
Console.WriteLine("=".PadRight(60, '='));
Console.WriteLine($"图层名称: {report.LayerName}");
Console.WriteLine($"要素数量: {report.FeatureCount}");
Console.WriteLine($"字段数量: {report.FieldCount}");
Console.WriteLine($"错误数量: {report.ErrorCount}");
Console.WriteLine($"警告数量: {report.WarningCount}");
Console.WriteLine();
if (report.Issues.Any())
{
Console.WriteLine("问题详情:");
Console.WriteLine("-".PadRight(60, '-'));
foreach (var issue in report.Issues)
{
Console.WriteLine($"[{issue.Severity}] FID {issue.Fid}: {issue.IssueType}");
Console.WriteLine($" {issue.Description}");
}
}
else
{
Console.WriteLine("未发现问题,数据质量良好!");
}
Console.WriteLine("=".PadRight(60, '='));
}
}
public class QualityReport
{
public string LayerName { get; set; }
public int FeatureCount { get; set; }
public int FieldCount { get; set; }
public int ErrorCount { get; set; }
public int WarningCount { get; set; }
public List<QualityIssue> Issues { get; set; } = new();
}
public class QualityIssue
{
public int Fid { get; set; }
public string IssueType { get; set; }
public string Description { get; set; }
public string Severity { get; set; }
}
11.7 小结
本章通过五个实战案例展示了OGU4Net的综合应用:
- 批量转换工具:演示了格式转换、编码处理、坐标转换
- 空间查询服务:展示了Web API集成、空间关系查询
- 国土数据处理:涵盖了TXT文件解析、多边形构建、面积计算
- 面积统计工具:实现了投影转换、分组统计
- 质量检查工具:包括几何验证、属性检查
这些案例涵盖了实际GIS开发中的常见场景,可以作为开发参考。

浙公网安备 33010602011771号