11-实战案例

第十一章:实战案例

11.1 概述

本章通过三个完整的实战案例,展示如何在实际项目中综合运用 geometry-api-net 的各项功能。

11.2 案例一:位置服务系统

11.2.1 需求描述

构建一个位置服务系统,实现以下功能:

  1. 查找用户附近的商店
  2. 判断用户是否在配送区域内
  3. 计算配送距离和预估时间
  4. 管理地理围栏告警

11.2.2 核心模型

using Esri.Geometry.Core;
using Esri.Geometry.Core.Geometries;
using Esri.Geometry.Core.SpatialReference;

// 商店实体
public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Point Location { get; set; }
    public Polygon DeliveryZone { get; set; }
    public bool IsOpen { get; set; }
}

// 用户位置
public class UserLocation
{
    public string UserId { get; set; }
    public Point Position { get; set; }
    public DateTime Timestamp { get; set; }
}

// 配送订单
public class DeliveryOrder
{
    public int OrderId { get; set; }
    public Store Store { get; set; }
    public Point DeliveryAddress { get; set; }
    public double DistanceMeters { get; set; }
    public int EstimatedMinutes { get; set; }
}

11.2.3 位置服务实现

public interface ILocationService
{
    List<Store> FindNearbyStores(Point userLocation, double radiusMeters);
    Store FindNearestStore(Point userLocation);
    bool IsInDeliveryZone(Store store, Point address);
    DeliveryOrder CalculateDelivery(Store store, Point address);
}

public class LocationService : ILocationService
{
    private readonly List<Store> _stores;
    private const double AVERAGE_SPEED_MPS = 10.0; // 平均速度 10 米/秒
    
    public LocationService(List<Store> stores)
    {
        _stores = stores ?? throw new ArgumentNullException(nameof(stores));
    }
    
    public List<Store> FindNearbyStores(Point userLocation, double radiusMeters)
    {
        var nearbyStores = new List<Store>();
        
        foreach (var store in _stores.Where(s => s.IsOpen))
        {
            double distance = GeometryEngine.GeodesicDistance(userLocation, store.Location);
            
            if (distance <= radiusMeters)
            {
                nearbyStores.Add(store);
            }
        }
        
        // 按距离排序
        return nearbyStores
            .OrderBy(s => GeometryEngine.GeodesicDistance(userLocation, s.Location))
            .ToList();
    }
    
    public Store FindNearestStore(Point userLocation)
    {
        Store nearest = null;
        double minDistance = double.MaxValue;
        
        foreach (var store in _stores.Where(s => s.IsOpen))
        {
            double distance = GeometryEngine.GeodesicDistance(userLocation, store.Location);
            
            if (distance < minDistance)
            {
                minDistance = distance;
                nearest = store;
            }
        }
        
        return nearest;
    }
    
    public bool IsInDeliveryZone(Store store, Point address)
    {
        if (store?.DeliveryZone == null || address == null)
            return false;
        
        return GeometryEngine.Contains(store.DeliveryZone, address);
    }
    
    public DeliveryOrder CalculateDelivery(Store store, Point address)
    {
        if (!IsInDeliveryZone(store, address))
        {
            throw new InvalidOperationException("地址不在配送范围内");
        }
        
        double distanceMeters = GeometryEngine.GeodesicDistance(store.Location, address);
        int estimatedMinutes = (int)Math.Ceiling(distanceMeters / AVERAGE_SPEED_MPS / 60);
        
        return new DeliveryOrder
        {
            Store = store,
            DeliveryAddress = address,
            DistanceMeters = distanceMeters,
            EstimatedMinutes = estimatedMinutes
        };
    }
}

11.2.4 地理围栏服务

public interface IGeofenceService
{
    void AddGeofence(string id, Polygon boundary);
    void RemoveGeofence(string id);
    List<string> CheckPosition(Point position);
    event EventHandler<GeofenceEventArgs> OnEnter;
    event EventHandler<GeofenceEventArgs> OnExit;
}

public class GeofenceEventArgs : EventArgs
{
    public string GeofenceId { get; set; }
    public string UserId { get; set; }
    public Point Position { get; set; }
    public bool IsEntry { get; set; }
}

public class GeofenceService : IGeofenceService
{
    private readonly Dictionary<string, Polygon> _geofences = new();
    private readonly Dictionary<string, HashSet<string>> _userInGeofence = new();
    
    public event EventHandler<GeofenceEventArgs> OnEnter;
    public event EventHandler<GeofenceEventArgs> OnExit;
    
    public void AddGeofence(string id, Polygon boundary)
    {
        _geofences[id] = boundary;
        _userInGeofence[id] = new HashSet<string>();
    }
    
    public void RemoveGeofence(string id)
    {
        _geofences.Remove(id);
        _userInGeofence.Remove(id);
    }
    
    public List<string> CheckPosition(Point position)
    {
        return _geofences
            .Where(kvp => GeometryEngine.Contains(kvp.Value, position))
            .Select(kvp => kvp.Key)
            .ToList();
    }
    
    public void UpdateUserPosition(string userId, Point position)
    {
        foreach (var kvp in _geofences)
        {
            var geofenceId = kvp.Key;
            var boundary = kvp.Value;
            var usersInside = _userInGeofence[geofenceId];
            
            bool isInside = GeometryEngine.Contains(boundary, position);
            bool wasInside = usersInside.Contains(userId);
            
            if (isInside && !wasInside)
            {
                // 进入围栏
                usersInside.Add(userId);
                OnEnter?.Invoke(this, new GeofenceEventArgs
                {
                    GeofenceId = geofenceId,
                    UserId = userId,
                    Position = position,
                    IsEntry = true
                });
            }
            else if (!isInside && wasInside)
            {
                // 离开围栏
                usersInside.Remove(userId);
                OnExit?.Invoke(this, new GeofenceEventArgs
                {
                    GeofenceId = geofenceId,
                    UserId = userId,
                    Position = position,
                    IsEntry = false
                });
            }
        }
    }
}

11.2.5 使用示例

// 初始化商店数据
var stores = new List<Store>
{
    new Store
    {
        Id = 1,
        Name = "总店",
        Location = new Point(116.4074, 39.9042),
        DeliveryZone = CreateCirclePolygon(116.4074, 39.9042, 0.02),
        IsOpen = true
    },
    new Store
    {
        Id = 2,
        Name = "分店A",
        Location = new Point(116.42, 39.91),
        DeliveryZone = CreateCirclePolygon(116.42, 39.91, 0.015),
        IsOpen = true
    }
};

var locationService = new LocationService(stores);

// 查找附近商店
var userLocation = new Point(116.41, 39.905);
var nearbyStores = locationService.FindNearbyStores(userLocation, 2000);
Console.WriteLine($"附近 2 公里内有 {nearbyStores.Count} 家商店");

// 计算配送
var nearestStore = locationService.FindNearestStore(userLocation);
if (locationService.IsInDeliveryZone(nearestStore, userLocation))
{
    var order = locationService.CalculateDelivery(nearestStore, userLocation);
    Console.WriteLine($"配送距离:{order.DistanceMeters:F0} 米");
    Console.WriteLine($"预计时间:{order.EstimatedMinutes} 分钟");
}

// 设置地理围栏
var geofenceService = new GeofenceService();
geofenceService.OnEnter += (s, e) => Console.WriteLine($"用户 {e.UserId} 进入 {e.GeofenceId}");
geofenceService.OnExit += (s, e) => Console.WriteLine($"用户 {e.UserId} 离开 {e.GeofenceId}");

geofenceService.AddGeofence("office", CreateCirclePolygon(116.4, 39.9, 0.005));
geofenceService.UpdateUserPosition("user1", new Point(116.4, 39.9));

// 辅助方法:创建圆形多边形(近似)
static Polygon CreateCirclePolygon(double centerX, double centerY, double radius)
{
    var polygon = new Polygon();
    var points = new List<Point>();
    const int segments = 32;
    
    for (int i = 0; i <= segments; i++)
    {
        double angle = 2 * Math.PI * i / segments;
        points.Add(new Point(
            centerX + radius * Math.Cos(angle),
            centerY + radius * Math.Sin(angle)
        ));
    }
    
    polygon.AddRing(points);
    return polygon;
}

11.3 案例二:轨迹分析系统

11.3.1 需求描述

构建一个 GPS 轨迹分析系统,实现以下功能:

  1. 轨迹数据的简化和平滑
  2. 计算行程距离和时间
  3. 识别停留点
  4. 轨迹与道路的匹配

11.3.2 轨迹分析服务

public class TrackPoint
{
    public Point Location { get; set; }
    public DateTime Timestamp { get; set; }
    public double? Speed { get; set; }  // 米/秒
}

public class TrackSegment
{
    public List<TrackPoint> Points { get; set; } = new();
    public double TotalDistanceMeters { get; set; }
    public TimeSpan Duration { get; set; }
    public double AverageSpeedMps { get; set; }
}

public class StayPoint
{
    public Point Location { get; set; }
    public DateTime ArrivalTime { get; set; }
    public DateTime DepartureTime { get; set; }
    public TimeSpan Duration { get; set; }
}

public interface ITrackAnalysisService
{
    Polyline SimplifyTrack(List<TrackPoint> points, double tolerance);
    TrackSegment AnalyzeSegment(List<TrackPoint> points);
    List<StayPoint> DetectStayPoints(List<TrackPoint> points, double radiusMeters, TimeSpan minDuration);
}

public class TrackAnalysisService : ITrackAnalysisService
{
    public Polyline SimplifyTrack(List<TrackPoint> points, double tolerance)
    {
        // 转换为 Polyline
        var polyline = new Polyline();
        polyline.AddPath(points.Select(p => p.Location).ToList());
        
        // 使用 Douglas-Peucker 算法简化
        var simplified = GeometryEngine.Simplify(polyline, tolerance);
        return simplified as Polyline ?? polyline;
    }
    
    public TrackSegment AnalyzeSegment(List<TrackPoint> points)
    {
        if (points == null || points.Count < 2)
            return new TrackSegment();
        
        double totalDistance = 0;
        
        for (int i = 1; i < points.Count; i++)
        {
            totalDistance += GeometryEngine.GeodesicDistance(
                points[i - 1].Location, 
                points[i].Location);
        }
        
        var duration = points.Last().Timestamp - points.First().Timestamp;
        
        return new TrackSegment
        {
            Points = points,
            TotalDistanceMeters = totalDistance,
            Duration = duration,
            AverageSpeedMps = duration.TotalSeconds > 0 
                ? totalDistance / duration.TotalSeconds 
                : 0
        };
    }
    
    public List<StayPoint> DetectStayPoints(
        List<TrackPoint> points, 
        double radiusMeters, 
        TimeSpan minDuration)
    {
        var stayPoints = new List<StayPoint>();
        
        int i = 0;
        while (i < points.Count)
        {
            int j = i + 1;
            var center = points[i].Location;
            
            // 查找在半径范围内的连续点
            while (j < points.Count)
            {
                double distance = GeometryEngine.GeodesicDistance(center, points[j].Location);
                if (distance > radiusMeters)
                    break;
                j++;
            }
            
            // 检查停留时间
            var duration = points[j - 1].Timestamp - points[i].Timestamp;
            if (duration >= minDuration)
            {
                // 计算停留点的平均位置
                double avgX = 0, avgY = 0;
                for (int k = i; k < j; k++)
                {
                    avgX += points[k].Location.X;
                    avgY += points[k].Location.Y;
                }
                avgX /= (j - i);
                avgY /= (j - i);
                
                stayPoints.Add(new StayPoint
                {
                    Location = new Point(avgX, avgY),
                    ArrivalTime = points[i].Timestamp,
                    DepartureTime = points[j - 1].Timestamp,
                    Duration = duration
                });
            }
            
            i = j;
        }
        
        return stayPoints;
    }
}

11.3.3 使用示例

// 模拟 GPS 轨迹数据
var trackPoints = new List<TrackPoint>
{
    new TrackPoint { Location = new Point(116.40, 39.90), Timestamp = DateTime.Parse("2024-01-01 09:00:00") },
    new TrackPoint { Location = new Point(116.41, 39.91), Timestamp = DateTime.Parse("2024-01-01 09:10:00") },
    new TrackPoint { Location = new Point(116.42, 39.92), Timestamp = DateTime.Parse("2024-01-01 09:20:00") },
    // 停留点
    new TrackPoint { Location = new Point(116.42, 39.92), Timestamp = DateTime.Parse("2024-01-01 09:30:00") },
    new TrackPoint { Location = new Point(116.421, 39.921), Timestamp = DateTime.Parse("2024-01-01 09:40:00") },
    new TrackPoint { Location = new Point(116.42, 39.92), Timestamp = DateTime.Parse("2024-01-01 09:50:00") },
    // 继续移动
    new TrackPoint { Location = new Point(116.45, 39.95), Timestamp = DateTime.Parse("2024-01-01 10:00:00") }
};

var service = new TrackAnalysisService();

// 分析轨迹
var segment = service.AnalyzeSegment(trackPoints);
Console.WriteLine($"总距离:{segment.TotalDistanceMeters:F0} 米");
Console.WriteLine($"总时长:{segment.Duration.TotalMinutes:F0} 分钟");
Console.WriteLine($"平均速度:{segment.AverageSpeedMps * 3.6:F1} 公里/小时");

// 检测停留点
var stayPoints = service.DetectStayPoints(trackPoints, 100, TimeSpan.FromMinutes(15));
Console.WriteLine($"\n发现 {stayPoints.Count} 个停留点:");
foreach (var sp in stayPoints)
{
    Console.WriteLine($"  位置:({sp.Location.X:F4}, {sp.Location.Y:F4})");
    Console.WriteLine($"  停留时间:{sp.Duration.TotalMinutes:F0} 分钟");
}

// 简化轨迹
var simplified = service.SimplifyTrack(trackPoints, 0.001);
Console.WriteLine($"\n原始点数:{trackPoints.Count}");
Console.WriteLine($"简化后点数:{simplified.GetPath(0).Count}");

11.4 案例三:区域分析系统

11.4.1 需求描述

构建一个区域分析系统,实现以下功能:

  1. 区域的叠加分析
  2. 区域统计
  3. 热力图数据生成
  4. 区域边界管理

11.4.2 区域分析服务

public class Region
{
    public string Id { get; set; }
    public string Name { get; set; }
    public Polygon Boundary { get; set; }
    public Dictionary<string, object> Properties { get; set; } = new();
}

public class OverlapResult
{
    public Region Region1 { get; set; }
    public Region Region2 { get; set; }
    public Geometry OverlapArea { get; set; }
    public double OverlapRatio { get; set; }
}

public interface IRegionAnalysisService
{
    List<OverlapResult> FindOverlaps(List<Region> regions);
    Region MergeRegions(List<Region> regions, string newId, string newName);
    Dictionary<string, int> PointsPerRegion(List<Region> regions, List<Point> points);
    double[,] GenerateHeatmapGrid(List<Point> points, Envelope bounds, int gridSizeX, int gridSizeY);
}

public class RegionAnalysisService : IRegionAnalysisService
{
    public List<OverlapResult> FindOverlaps(List<Region> regions)
    {
        var results = new List<OverlapResult>();
        
        for (int i = 0; i < regions.Count; i++)
        {
            for (int j = i + 1; j < regions.Count; j++)
            {
                // 快速过滤
                var env1 = regions[i].Boundary.GetEnvelope();
                var env2 = regions[j].Boundary.GetEnvelope();
                
                if (!env1.Intersects(env2))
                    continue;
                
                // 精确检查
                if (GeometryEngine.Intersects(regions[i].Boundary, regions[j].Boundary))
                {
                    var overlap = GeometryEngine.Intersection(
                        regions[i].Boundary, 
                        regions[j].Boundary);
                    
                    if (!overlap.IsEmpty)
                    {
                        double overlapArea = GeometryEngine.Area(overlap);
                        double totalArea = regions[i].Boundary.Area + regions[j].Boundary.Area;
                        
                        results.Add(new OverlapResult
                        {
                            Region1 = regions[i],
                            Region2 = regions[j],
                            OverlapArea = overlap,
                            OverlapRatio = overlapArea / (totalArea - overlapArea)
                        });
                    }
                }
            }
        }
        
        return results;
    }
    
    public Region MergeRegions(List<Region> regions, string newId, string newName)
    {
        if (regions == null || regions.Count == 0)
            throw new ArgumentException("No regions to merge");
        
        Geometry merged = regions[0].Boundary;
        
        for (int i = 1; i < regions.Count; i++)
        {
            merged = GeometryEngine.Union(merged, regions[i].Boundary);
        }
        
        // 将结果转换为 Polygon
        var envelope = merged.GetEnvelope();
        var polygon = new Polygon();
        polygon.AddRing(new List<Point>
        {
            new Point(envelope.XMin, envelope.YMin),
            new Point(envelope.XMax, envelope.YMin),
            new Point(envelope.XMax, envelope.YMax),
            new Point(envelope.XMin, envelope.YMax),
            new Point(envelope.XMin, envelope.YMin)
        });
        
        return new Region
        {
            Id = newId,
            Name = newName,
            Boundary = polygon
        };
    }
    
    public Dictionary<string, int> PointsPerRegion(List<Region> regions, List<Point> points)
    {
        var result = new Dictionary<string, int>();
        
        // 预计算包络矩形
        var envelopes = regions.ToDictionary(r => r.Id, r => r.Boundary.GetEnvelope());
        
        foreach (var region in regions)
        {
            result[region.Id] = 0;
        }
        
        foreach (var point in points)
        {
            foreach (var region in regions)
            {
                // 快速过滤
                if (!envelopes[region.Id].Contains(point))
                    continue;
                
                // 精确检查
                if (GeometryEngine.Contains(region.Boundary, point))
                {
                    result[region.Id]++;
                    break;  // 假设点只能在一个区域内
                }
            }
        }
        
        return result;
    }
    
    public double[,] GenerateHeatmapGrid(
        List<Point> points, 
        Envelope bounds, 
        int gridSizeX, 
        int gridSizeY)
    {
        var grid = new double[gridSizeY, gridSizeX];
        double cellWidth = bounds.Width / gridSizeX;
        double cellHeight = bounds.Height / gridSizeY;
        
        foreach (var point in points)
        {
            if (!bounds.Contains(point))
                continue;
            
            int cellX = (int)((point.X - bounds.XMin) / cellWidth);
            int cellY = (int)((point.Y - bounds.YMin) / cellHeight);
            
            cellX = Math.Clamp(cellX, 0, gridSizeX - 1);
            cellY = Math.Clamp(cellY, 0, gridSizeY - 1);
            
            grid[cellY, cellX]++;
        }
        
        // 归一化
        double maxValue = 0;
        for (int y = 0; y < gridSizeY; y++)
        {
            for (int x = 0; x < gridSizeX; x++)
            {
                maxValue = Math.Max(maxValue, grid[y, x]);
            }
        }
        
        if (maxValue > 0)
        {
            for (int y = 0; y < gridSizeY; y++)
            {
                for (int x = 0; x < gridSizeX; x++)
                {
                    grid[y, x] /= maxValue;
                }
            }
        }
        
        return grid;
    }
}

11.4.3 使用示例

// 创建区域
var regions = new List<Region>
{
    new Region
    {
        Id = "A",
        Name = "区域 A",
        Boundary = CreateRectangle(0, 0, 50, 50)
    },
    new Region
    {
        Id = "B",
        Name = "区域 B",
        Boundary = CreateRectangle(40, 40, 100, 100)
    },
    new Region
    {
        Id = "C",
        Name = "区域 C",
        Boundary = CreateRectangle(80, 0, 120, 40)
    }
};

var service = new RegionAnalysisService();

// 查找重叠区域
var overlaps = service.FindOverlaps(regions);
Console.WriteLine($"发现 {overlaps.Count} 个重叠区域:");
foreach (var overlap in overlaps)
{
    Console.WriteLine($"  {overlap.Region1.Name} 与 {overlap.Region2.Name},重叠比例:{overlap.OverlapRatio:P2}");
}

// 随机生成一些点
var random = new Random(42);
var points = Enumerable.Range(0, 1000)
    .Select(_ => new Point(random.NextDouble() * 120, random.NextDouble() * 100))
    .ToList();

// 统计每个区域的点数
var pointCounts = service.PointsPerRegion(regions, points);
Console.WriteLine("\n各区域点数统计:");
foreach (var kvp in pointCounts)
{
    Console.WriteLine($"  {kvp.Key}: {kvp.Value} 个点");
}

// 生成热力图数据
var bounds = new Envelope(0, 0, 120, 100);
var heatmap = service.GenerateHeatmapGrid(points, bounds, 12, 10);

Console.WriteLine("\n热力图数据(10x12 网格):");
for (int y = 9; y >= 0; y--)
{
    for (int x = 0; x < 12; x++)
    {
        char c = heatmap[y, x] switch
        {
            >= 0.8 => '█',
            >= 0.6 => '▓',
            >= 0.4 => '▒',
            >= 0.2 => '░',
            _ => ' '
        };
        Console.Write(c);
    }
    Console.WriteLine();
}

static Polygon CreateRectangle(double x1, double y1, double x2, double y2)
{
    var polygon = new Polygon();
    polygon.AddRing(new List<Point>
    {
        new Point(x1, y1),
        new Point(x2, y1),
        new Point(x2, y2),
        new Point(x1, y2),
        new Point(x1, y1)
    });
    return polygon;
}

11.5 小结

本章通过三个实战案例展示了 geometry-api-net 在实际项目中的应用:

  1. 位置服务系统:商店查找、配送区域判断、地理围栏
  2. 轨迹分析系统:轨迹简化、距离计算、停留点检测
  3. 区域分析系统:区域叠加、点统计、热力图生成

这些案例综合运用了:

  • 几何对象的创建和操作
  • 空间关系测试(Contains、Intersects)
  • 几何运算(Simplify、Buffer、Union、Intersection)
  • 大地测量计算(GeodesicDistance)
  • 邻近分析(GetNearestCoordinate)

通过这些实践,您应该能够更好地理解如何在实际项目中应用 geometry-api-net。

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