第15章-实战案例与最佳实践

第15章:实战案例与最佳实践

15.1 案例一:地理围栏服务

15.1.1 场景描述

构建一个地理围栏服务,支持:

  • 定义多个地理围栏区域
  • 判断设备位置是否在围栏内
  • 触发进入/离开事件

15.1.2 实现代码

using NetTopologySuite.Geometries;
using NetTopologySuite.Geometries.Prepared;
using System.Collections.Concurrent;

public class GeofenceService
{
    private readonly GeometryFactory _factory;
    private readonly ConcurrentDictionary<string, GeofenceArea> _geofences;
    private readonly ConcurrentDictionary<string, string?> _deviceStates;

    public GeofenceService()
    {
        _factory = new GeometryFactory(new PrecisionModel(), 4326);
        _geofences = new ConcurrentDictionary<string, GeofenceArea>();
        _deviceStates = new ConcurrentDictionary<string, string?>();
    }

    /// <summary>
    /// 添加圆形围栏
    /// </summary>
    public void AddCircularGeofence(string id, double centerLon, double centerLat, double radiusMeters)
    {
        var center = _factory.CreatePoint(new Coordinate(centerLon, centerLat));
        // 近似转换:1度 ≈ 111公里
        var radiusDegrees = radiusMeters / 111000.0;
        var circle = center.Buffer(radiusDegrees, 32);
        
        AddGeofence(id, circle);
    }

    /// <summary>
    /// 添加多边形围栏
    /// </summary>
    public void AddPolygonGeofence(string id, Coordinate[] coordinates)
    {
        // 确保闭合
        if (!coordinates[0].Equals2D(coordinates[^1]))
        {
            coordinates = coordinates.Append(coordinates[0]).ToArray();
        }
        
        var polygon = _factory.CreatePolygon(coordinates);
        AddGeofence(id, polygon);
    }

    private void AddGeofence(string id, Geometry geometry)
    {
        var prepared = PreparedGeometryFactory.Prepare(geometry);
        _geofences[id] = new GeofenceArea
        {
            Id = id,
            Geometry = geometry,
            PreparedGeometry = prepared
        };
    }

    /// <summary>
    /// 检查设备位置
    /// </summary>
    public GeofenceEvent? CheckDeviceLocation(string deviceId, double lon, double lat)
    {
        var point = _factory.CreatePoint(new Coordinate(lon, lat));
        
        string? currentGeofence = null;
        foreach (var geofence in _geofences.Values)
        {
            if (geofence.PreparedGeometry.Contains(point))
            {
                currentGeofence = geofence.Id;
                break;
            }
        }

        // 获取之前的状态
        _deviceStates.TryGetValue(deviceId, out var previousGeofence);
        _deviceStates[deviceId] = currentGeofence;

        // 判断事件类型
        if (previousGeofence != currentGeofence)
        {
            if (currentGeofence != null && previousGeofence == null)
            {
                return new GeofenceEvent
                {
                    DeviceId = deviceId,
                    GeofenceId = currentGeofence,
                    EventType = GeofenceEventType.Enter,
                    Timestamp = DateTime.UtcNow
                };
            }
            else if (currentGeofence == null && previousGeofence != null)
            {
                return new GeofenceEvent
                {
                    DeviceId = deviceId,
                    GeofenceId = previousGeofence,
                    EventType = GeofenceEventType.Exit,
                    Timestamp = DateTime.UtcNow
                };
            }
            else if (currentGeofence != null && previousGeofence != null)
            {
                return new GeofenceEvent
                {
                    DeviceId = deviceId,
                    GeofenceId = currentGeofence,
                    PreviousGeofenceId = previousGeofence,
                    EventType = GeofenceEventType.Transfer,
                    Timestamp = DateTime.UtcNow
                };
            }
        }

        return null;
    }

    /// <summary>
    /// 批量检查设备位置
    /// </summary>
    public List<GeofenceEvent> CheckDeviceLocations(
        IEnumerable<(string DeviceId, double Lon, double Lat)> locations)
    {
        var events = new ConcurrentBag<GeofenceEvent>();

        Parallel.ForEach(locations, location =>
        {
            var evt = CheckDeviceLocation(location.DeviceId, location.Lon, location.Lat);
            if (evt != null)
            {
                events.Add(evt);
            }
        });

        return events.ToList();
    }
}

public class GeofenceArea
{
    public string Id { get; set; } = "";
    public Geometry Geometry { get; set; } = null!;
    public IPreparedGeometry PreparedGeometry { get; set; } = null!;
}

public class GeofenceEvent
{
    public string DeviceId { get; set; } = "";
    public string GeofenceId { get; set; } = "";
    public string? PreviousGeofenceId { get; set; }
    public GeofenceEventType EventType { get; set; }
    public DateTime Timestamp { get; set; }
}

public enum GeofenceEventType
{
    Enter,
    Exit,
    Transfer
}

// 使用示例
var geofenceService = new GeofenceService();

// 添加围栏
geofenceService.AddCircularGeofence("office", 116.4074, 39.9042, 500);
geofenceService.AddPolygonGeofence("warehouse", new Coordinate[]
{
    new Coordinate(116.3, 39.8),
    new Coordinate(116.32, 39.8),
    new Coordinate(116.32, 39.82),
    new Coordinate(116.3, 39.82)
});

// 检查位置
var evt = geofenceService.CheckDeviceLocation("device001", 116.4074, 39.9042);
if (evt != null)
{
    Console.WriteLine($"设备 {evt.DeviceId} {evt.EventType} 围栏 {evt.GeofenceId}");
}

15.2 案例二:路径规划服务

15.2.1 场景描述

实现一个简单的路径分析服务,支持:

  • 计算两点之间的直线距离
  • 沿路线等分点
  • 路线缓冲区分析
public class RouteAnalysisService
{
    private readonly GeometryFactory _factory;

    public RouteAnalysisService()
    {
        _factory = new GeometryFactory(new PrecisionModel(), 4326);
    }

    /// <summary>
    /// 创建路线
    /// </summary>
    public LineString CreateRoute(IEnumerable<(double Lon, double Lat)> waypoints)
    {
        var coords = waypoints.Select(p => new Coordinate(p.Lon, p.Lat)).ToArray();
        return _factory.CreateLineString(coords);
    }

    /// <summary>
    /// 计算路线长度(公里)
    /// </summary>
    public double CalculateRouteLength(LineString route)
    {
        double totalDistance = 0;
        var coords = route.Coordinates;
        
        for (int i = 0; i < coords.Length - 1; i++)
        {
            totalDistance += HaversineDistance(
                coords[i].Y, coords[i].X,
                coords[i + 1].Y, coords[i + 1].X);
        }
        
        return totalDistance;
    }

    /// <summary>
    /// 沿路线生成等距点
    /// </summary>
    public List<Point> GenerateEquidistantPoints(LineString route, int pointCount)
    {
        var indexedLine = new NetTopologySuite.LinearReferencing.LengthIndexedLine(route);
        var totalLength = route.Length;
        var interval = totalLength / (pointCount - 1);
        
        var points = new List<Point>();
        for (int i = 0; i < pointCount; i++)
        {
            var coord = indexedLine.ExtractPoint(i * interval);
            points.Add(_factory.CreatePoint(coord));
        }
        
        return points;
    }

    /// <summary>
    /// 计算路线缓冲区
    /// </summary>
    public Polygon CreateRouteBuffer(LineString route, double bufferDistanceDegrees)
    {
        return (Polygon)route.Buffer(bufferDistanceDegrees);
    }

    /// <summary>
    /// 查找路线上最近的点
    /// </summary>
    public (Point NearestPoint, double Distance, double PositionAlongRoute) 
        FindNearestPointOnRoute(LineString route, Point queryPoint)
    {
        var indexedLine = new NetTopologySuite.LinearReferencing.LengthIndexedLine(route);
        var position = indexedLine.Project(queryPoint.Coordinate);
        var nearestCoord = indexedLine.ExtractPoint(position);
        
        var nearestPoint = _factory.CreatePoint(nearestCoord);
        var distance = HaversineDistance(
            queryPoint.Y, queryPoint.X,
            nearestCoord.Y, nearestCoord.X);
        
        return (nearestPoint, distance, position / route.Length * 100);
    }

    /// <summary>
    /// 提取路线的一部分
    /// </summary>
    public LineString ExtractSubRoute(LineString route, double startPercent, double endPercent)
    {
        var indexedLine = new NetTopologySuite.LinearReferencing.LengthIndexedLine(route);
        var totalLength = route.Length;
        
        var startIndex = totalLength * startPercent / 100;
        var endIndex = totalLength * endPercent / 100;
        
        return (LineString)indexedLine.ExtractLine(startIndex, endIndex);
    }

    private double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
    {
        const double R = 6371; // 地球半径(公里)
        
        var dLat = ToRadians(lat2 - lat1);
        var dLon = ToRadians(lon2 - lon1);
        
        var 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);
        
        var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
        
        return R * c;
    }

    private double ToRadians(double degrees) => degrees * Math.PI / 180;
}

// 使用示例
var routeService = new RouteAnalysisService();

// 创建路线
var route = routeService.CreateRoute(new[]
{
    (116.4074, 39.9042),  // 北京
    (117.2, 39.1),       // 天津
    (121.4737, 31.2304)  // 上海
});

// 计算长度
var length = routeService.CalculateRouteLength(route);
Console.WriteLine($"路线长度: {length:F2} 公里");

// 生成等距点
var points = routeService.GenerateEquidistantPoints(route, 10);
Console.WriteLine($"生成了 {points.Count} 个等距点");

// 创建缓冲区(约10公里)
var buffer = routeService.CreateRouteBuffer(route, 0.09);
Console.WriteLine($"缓冲区面积: {buffer.Area:F4} 平方度");

15.3 案例三:空间数据 ETL

15.3.1 场景描述

实现一个空间数据 ETL 服务,支持:

  • 从多种格式读取数据
  • 数据转换和清洗
  • 导出到目标格式
public class SpatialETLService
{
    private readonly GeometryFactory _factory;
    private readonly CoordinateTransformService _transformService;

    public SpatialETLService()
    {
        _factory = new GeometryFactory(new PrecisionModel(), 4326);
        _transformService = new CoordinateTransformService();
    }

    /// <summary>
    /// ETL 作业配置
    /// </summary>
    public class ETLJob
    {
        public string SourcePath { get; set; } = "";
        public string SourceFormat { get; set; } = "";
        public string TargetPath { get; set; } = "";
        public string TargetFormat { get; set; } = "";
        public int? SourceSrid { get; set; }
        public int? TargetSrid { get; set; }
        public Func<Feature, Feature?>? TransformFunc { get; set; }
        public Func<Feature, bool>? FilterFunc { get; set; }
    }

    /// <summary>
    /// 执行 ETL 作业
    /// </summary>
    public ETLResult ExecuteJob(ETLJob job)
    {
        var result = new ETLResult();
        var sw = System.Diagnostics.Stopwatch.StartNew();

        try
        {
            // 读取源数据
            var features = ReadFeatures(job.SourcePath, job.SourceFormat);
            result.SourceCount = features.Count;

            // 过滤
            if (job.FilterFunc != null)
            {
                features = features.Where(job.FilterFunc).ToList();
            }
            result.FilteredCount = features.Count;

            // 转换
            var transformedFeatures = new List<Feature>();
            foreach (var feature in features)
            {
                try
                {
                    var transformed = job.TransformFunc != null 
                        ? job.TransformFunc(feature) 
                        : feature;
                    
                    if (transformed == null) continue;

                    // 坐标转换
                    if (job.SourceSrid.HasValue && job.TargetSrid.HasValue && 
                        job.SourceSrid != job.TargetSrid)
                    {
                        transformed = new Feature(
                            _transformService.TransformGeometry(
                                transformed.Geometry, job.TargetSrid.Value),
                            transformed.Attributes);
                    }

                    transformedFeatures.Add(transformed);
                }
                catch (Exception ex)
                {
                    result.Errors.Add($"转换错误: {ex.Message}");
                }
            }
            result.TransformedCount = transformedFeatures.Count;

            // 写入目标
            WriteFeatures(transformedFeatures, job.TargetPath, job.TargetFormat);
            result.OutputCount = transformedFeatures.Count;
            result.Success = true;
        }
        catch (Exception ex)
        {
            result.Success = false;
            result.Errors.Add($"作业错误: {ex.Message}");
        }

        result.ElapsedMilliseconds = sw.ElapsedMilliseconds;
        return result;
    }

    private List<Feature> ReadFeatures(string path, string format)
    {
        return format.ToLower() switch
        {
            "shp" or "shapefile" => Shapefile.ReadAllFeatures(path)
                .Select(f => new Feature(f.Geometry, f.Attributes)).ToList(),
            "geojson" or "json" => ReadGeoJson(path),
            _ => throw new NotSupportedException($"不支持的格式: {format}")
        };
    }

    private List<Feature> ReadGeoJson(string path)
    {
        var json = File.ReadAllText(path);
        var reader = new NetTopologySuite.IO.GeoJsonReader();
        var collection = reader.Read<FeatureCollection>(json);
        return collection.Select(f => (Feature)f).ToList();
    }

    private void WriteFeatures(List<Feature> features, string path, string format)
    {
        switch (format.ToLower())
        {
            case "shp":
            case "shapefile":
                Shapefile.WriteAllFeatures(features, path);
                break;
            case "geojson":
            case "json":
                WriteGeoJson(features, path);
                break;
            default:
                throw new NotSupportedException($"不支持的格式: {format}");
        }
    }

    private void WriteGeoJson(List<Feature> features, string path)
    {
        var collection = new FeatureCollection();
        foreach (var f in features)
        {
            collection.Add(f);
        }
        
        var writer = new NetTopologySuite.IO.GeoJsonWriter();
        var json = writer.Write(collection);
        File.WriteAllText(path, json);
    }
}

public class ETLResult
{
    public bool Success { get; set; }
    public int SourceCount { get; set; }
    public int FilteredCount { get; set; }
    public int TransformedCount { get; set; }
    public int OutputCount { get; set; }
    public long ElapsedMilliseconds { get; set; }
    public List<string> Errors { get; set; } = new();
}

// 使用示例
var etlService = new SpatialETLService();

var job = new SpatialETLService.ETLJob
{
    SourcePath = "input.shp",
    SourceFormat = "shp",
    TargetPath = "output.geojson",
    TargetFormat = "geojson",
    SourceSrid = 4326,
    TargetSrid = 3857,
    FilterFunc = f => f.Geometry.IsValid && !f.Geometry.IsEmpty,
    TransformFunc = f =>
    {
        // 简化几何
        var simplified = f.Geometry.Simplify(0.001);
        // 添加计算属性
        var attrs = new AttributesTable();
        foreach (var name in f.Attributes.GetNames())
        {
            attrs.Add(name, f.Attributes[name]);
        }
        attrs.Add("area", simplified.Area);
        attrs.Add("processed_at", DateTime.UtcNow.ToString("o"));
        
        return new Feature(simplified, attrs);
    }
};

var result = etlService.ExecuteJob(job);
Console.WriteLine($"ETL 完成: {result.Success}");
Console.WriteLine($"输入: {result.SourceCount}, 输出: {result.OutputCount}");
Console.WriteLine($"耗时: {result.ElapsedMilliseconds}ms");

15.4 最佳实践总结

15.4.1 几何创建

// ✅ 使用工厂创建几何
var factory = new GeometryFactory(new PrecisionModel(), 4326);
var point = factory.CreatePoint(new Coordinate(lon, lat));

// ✅ 设置合适的 SRID
var factory4326 = new GeometryFactory(new PrecisionModel(), 4326);
var factory3857 = new GeometryFactory(new PrecisionModel(), 3857);

// ✅ 确保多边形闭合
var coords = new Coordinate[] { /* ... */ };
if (!coords[0].Equals2D(coords[^1]))
{
    coords = coords.Append(coords[0]).ToArray();
}

15.4.2 性能优化

// ✅ 使用 PreparedGeometry 进行重复查询
var prepared = PreparedGeometryFactory.Prepare(polygon);
var results = points.Where(p => prepared.Contains(p));

// ✅ 使用空间索引
var tree = new STRtree<Geometry>();
foreach (var geom in geometries)
{
    tree.Insert(geom.EnvelopeInternal, geom);
}
tree.Build();

// ✅ 并行处理大量数据
var results = geometries.AsParallel()
    .Select(g => g.Buffer(distance))
    .ToList();

15.4.3 数据验证

// ✅ 验证几何有效性
if (!geometry.IsValid)
{
    // 尝试修复
    geometry = geometry.Buffer(0);
}

// ✅ 检查空几何
if (geometry.IsEmpty)
{
    // 处理空几何
}

// ✅ 使用详细验证
var validator = new IsValidOp(geometry);
if (!validator.IsValid)
{
    var error = validator.ValidationError;
    Console.WriteLine($"错误: {error.ErrorType} at {error.Coordinate}");
}

15.4.4 内存管理

// ✅ 使用流式处理大文件
foreach (var feature in StreamFeatures(largefile))
{
    ProcessFeature(feature);
}

// ✅ 分批处理
ProcessInBatches(features, 1000, batch =>
{
    // 处理批次
});

// ✅ 及时释放资源
using var reader = Shapefile.OpenRead(path);

15.5 本章小结

本章通过实战案例展示了 NetTopologySuite 的实际应用:

  1. 地理围栏服务:设备定位和围栏判断
  2. 路径规划服务:路线分析和处理
  3. 空间数据 ETL:数据转换和清洗
  4. 最佳实践:几何创建、性能优化、数据验证、内存管理

15.6 教程总结

通过本教程的学习,您应该掌握了:

  1. NetTopologySuite 的核心概念和架构
  2. 几何对象的创建和操作
  3. 空间关系判断和几何运算
  4. 空间分析算法
  5. 各种数据格式的读写
  6. 数据库集成和 ORM 使用
  7. 坐标转换和投影
  8. 性能优化技巧
  9. 实际项目中的最佳实践

祝您在 GIS 开发之路上一切顺利!


相关资源

社区支持

posted @ 2025-12-29 10:22  我才是银古  阅读(2)  评论(0)    收藏  举报