第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 的实际应用:
- 地理围栏服务:设备定位和围栏判断
- 路径规划服务:路线分析和处理
- 空间数据 ETL:数据转换和清洗
- 最佳实践:几何创建、性能优化、数据验证、内存管理
15.6 教程总结
通过本教程的学习,您应该掌握了:
- NetTopologySuite 的核心概念和架构
- 几何对象的创建和操作
- 空间关系判断和几何运算
- 空间分析算法
- 各种数据格式的读写
- 数据库集成和 ORM 使用
- 坐标转换和投影
- 性能优化技巧
- 实际项目中的最佳实践
祝您在 GIS 开发之路上一切顺利!
相关资源:
社区支持:
- QQ群:289280914
- GitHub:@znlgis
- Gitee:@znlgis
- Bilibili:space/161342702

浙公网安备 33010602011771号