第五章:几何运算操作符

5.1 概述

几何运算操作符用于对几何对象进行变换和计算,生成新的几何对象或计算几何属性。这些操作符是空间分析的核心工具。

5.1.1 操作符分类

类别 操作符 说明
创建型 BufferOperator 创建缓冲区
创建型 ConvexHullOperator 计算凸包
创建型 BoundaryOperator 获取边界
创建型 OffsetOperator 创建偏移几何
计算型 AreaOperator 计算面积
计算型 LengthOperator 计算长度
计算型 CentroidOperator 计算质心
变换型 SimplifyOperator 简化几何
变换型 GeneralizeOperator 概化几何
变换型 DensifyOperator 密化几何
变换型 ClipOperator 裁剪几何

5.2 Buffer(缓冲区)

5.2.1 概念

缓冲区操作在几何对象周围创建指定距离的区域。这是 GIS 分析中最常用的操作之一。

应用场景:

  • 创建服务区域(如配送范围)
  • 环境影响分析(如噪音影响范围)
  • 安全距离分析(如河流泛滥区)

5.2.2 API

public class BufferOperator : IGeometryOperator<Polygon>
{
    // 整圆离散段数默认值(Esri Java 默认每 6° 一段 = 60;此处取 100 以提高精度)
    public const int DefaultCircleSegments = 100;

    public static BufferOperator Instance { get; }

    // 便捷重载:使用默认圆离散度
    public Polygon Execute(Geometry geometry, double distance,
                           SpatialReference? spatialRef = null);

    // 精确控制圆的离散段数(最小值会被钳制为 8)
    public Polygon Execute(Geometry geometry, double distance, int circleSegments,
                           SpatialReference? spatialRef = null);
}

GeometryEngine.Buffer(geometry, distance) 使用默认 DefaultCircleSegments;需要更高/更低精度时,直接调用 BufferOperator.Instance.Execute(geometry, distance, circleSegments)。负距离实现内缩(erosion)。

5.2.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 点缓冲区
var point = new Point(100, 100);
var buffer = GeometryEngine.Buffer(point, 10);

Console.WriteLine($"缓冲区类型:{buffer.Type}");  // Polygon
Console.WriteLine($"环数:{buffer.RingCount}");   // 1

// 包络矩形缓冲区
var envelope = new Envelope(0, 0, 50, 30);
var envBuffer = GeometryEngine.Buffer(envelope, 5);

// 缓冲区扩展了各方向 5 个单位
var bufferEnv = envBuffer.GetEnvelope();
Console.WriteLine($"缓冲区范围:({bufferEnv.XMin}, {bufferEnv.YMin}) - ({bufferEnv.XMax}, {bufferEnv.YMax})");
// 输出:(-5, -5) - (55, 35)

5.2.4 实现原理

Buffer 现在实现为真正的圆形缓冲区——即以半径 distance 的圆盘对几何做 Minkowski 和,圆按 circleSegments 段离散化为正多边形,顶点处用圆弧(round join)连接。核心思路如下:

输入类型 缓冲结果
Point / MultiPoint 以该点为圆心的正多边形圆(CircleRing)
Line / Polyline 每条线段生成两端半圆 + 两条平行边构成的胶囊(StadiumRing),线段端点补 CircleRing,再对所有片元做并
Polygon / Envelope 对每个环向外偏移得到 OffsetRing,再经稳健裁剪清理(保留绕数非零区域、按最小包含关系归并洞)
负距离 向内收缩(erosion,BufferNegative)
public Polygon Execute(Geometry geometry, double distance, int circleSegments,
                       SpatialReference? spatialRef = null)
{
    if (geometry.IsEmpty) return new Polygon();
    if (distance == 0)    return geometry.Copy();
    if (circleSegments < 8) circleSegments = 8;

    switch (geometry)
    {
        case Point p:
            // 点 → 正多边形圆
            return ToPolygon(new List<Ring> { CircleRing(p.X, p.Y, Math.Abs(distance), circleSegments) });

        case Polyline line:
            // 每段 → 胶囊(Stadium)+ 端点圆,随后并集
            var rings = new List<Ring>();
            foreach (var (a, b) in line.Segments())
            {
                rings.Add(StadiumRing(a.X, a.Y, b.X, b.Y, Math.Abs(distance), circleSegments));
                rings.Add(CircleRing(a.X, a.Y, Math.Abs(distance), circleSegments));
            }
            return UnionRings(rings);

        case Polygon polygon when distance < 0:
            return BufferNegative(polygon.Rings(), -distance, circleSegments);   // 内缩

        case Polygon polygon:
            return BufferPolygon(polygon.Rings(), distance, circleSegments);     // 外扩 + 稳健清理
        // Envelope 走 Polygon 分支
    }
    ...
}

对于复杂多边形(自交、近相切接缝、大量凹角),单靠通用叠加不够稳健:实现采用双策略并以“输出面积 ⊇ 原面积、不越 Steiner 上界、且真正覆盖原形所有顶点”作为不变式门控——先尝试 snap-rounding 平面叠加(SnapRoundOverlay.RobustUnion,把交点吸附到网格以消除尺度抖动造成的假接缝),失败时回退到片元顺序折叠(把向内缩的条带 quad 与凸角圆盘切成简单片元,逐批布尔合并)。这样既保证简单场景不降速,又让重度自交的偏移环也能得到稳健结果,并与 PostGIS/GEOS 对拍验证。

提示:早期版本对点/包络采用“正方形缓冲区”的简化实现,现已全部替换为上述真圆缓冲。

5.2.5 应用示例

// 场景:创建商店的配送范围
var storeLocation = new Point(116.4074, 39.9042);
var deliveryRadius = 0.02;  // 约 2 公里(经纬度)

var deliveryZone = GeometryEngine.Buffer(storeLocation, deliveryRadius);

// 检查客户是否在配送范围内
var customerLocation = new Point(116.41, 39.91);
bool canDeliver = GeometryEngine.Contains(deliveryZone, customerLocation);
Console.WriteLine($"可以配送:{canDeliver}");

5.3 ConvexHull(凸包)

5.3.1 概念

凸包是包含给定点集的最小凸多边形。可以想象为用橡皮筋套住一组钉子形成的形状。

应用场景:

  • 计算点集的最小外接多边形
  • 碰撞检测的预处理
  • 地理范围估计

5.3.2 API

public class ConvexHullOperator : IGeometryOperator<Geometry>
{
    public static ConvexHullOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, 
                           SpatialReference? spatialRef = null);
}

5.3.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 多点的凸包
var multiPoint = new MultiPoint();
multiPoint.Add(new Point(0, 0));
multiPoint.Add(new Point(10, 0));
multiPoint.Add(new Point(5, 5));
multiPoint.Add(new Point(10, 10));
multiPoint.Add(new Point(0, 10));
multiPoint.Add(new Point(5, 3));  // 内部点

var hull = GeometryEngine.ConvexHull(multiPoint);

if (hull is Polygon polygon)
{
    Console.WriteLine($"凸包顶点数:{polygon.GetRing(0).Count}");
    // 内部点 (5, 3) 不在凸包边界上
}

// 折线的凸包
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(5, 10),
    new Point(10, 0)
});

var lineHull = GeometryEngine.ConvexHull(polyline);  // 返回三角形

5.3.4 实现原理:Graham 扫描算法

private List<Point> GrahamScan(List<Point> points)
{
    if (points.Count < 3) return points;

    // 1. 找到 Y 坐标最小的点(最低点)
    var lowestPoint = points.OrderBy(p => p.Y).ThenBy(p => p.X).First();

    // 2. 按极角排序其他点
    var sortedPoints = points
        .Where(p => p != lowestPoint)
        .OrderBy(p => Math.Atan2(p.Y - lowestPoint.Y, p.X - lowestPoint.X))
        .ThenBy(p => p.Distance(lowestPoint))
        .ToList();

    // 3. 构建凸包
    var hull = new List<Point> { lowestPoint };

    foreach (var point in sortedPoints)
    {
        // 移除产生右转的点
        while (hull.Count > 1 && !IsLeftTurn(hull[hull.Count - 2], hull[hull.Count - 1], point))
        {
            hull.RemoveAt(hull.Count - 1);
        }
        hull.Add(point);
    }

    return hull;
}

private bool IsLeftTurn(Point p1, Point p2, Point p3)
{
    // 使用叉积判断是否左转
    var cross = (p2.X - p1.X) * (p3.Y - p1.Y) - (p2.Y - p1.Y) * (p3.X - p1.X);
    return cross > 0;
}

算法复杂度:O(n log n),其中 n 是点的数量。

5.3.5 返回类型

根据输入几何的不同,凸包返回不同类型:

输入点数 返回类型
0 空 Polygon
1 Point
2 Line
≥3 Polygon

5.4 Area 和 Length(面积和长度)

5.4.1 概念

  • Area(面积):计算多边形或包络矩形的二维面积
  • Length(长度):计算折线的总长度或多边形的周长

5.4.2 API

public class AreaOperator : IGeometryOperator<double>
{
    public static AreaOperator Instance { get; }
    
    public double Execute(Geometry geometry, 
                         SpatialReference? spatialRef = null);
}

public class LengthOperator : IGeometryOperator<double>
{
    public static LengthOperator Instance { get; }
    
    public double Execute(Geometry geometry, 
                         SpatialReference? spatialRef = null);
}

5.4.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 计算多边形面积
var polygon = new Polygon();
polygon.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(10, 0),
    new Point(10, 10),
    new Point(0, 10),
    new Point(0, 0)
});

double area = GeometryEngine.Area(polygon);
Console.WriteLine($"多边形面积:{area}");  // 100

// 计算包络矩形面积
var envelope = new Envelope(0, 0, 100, 50);
double envArea = GeometryEngine.Area(envelope);
Console.WriteLine($"包络面积:{envArea}");  // 5000

// 计算折线长度
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(3, 4),  // 距离原点 5
    new Point(3, 14)  // 向上 10
});

double length = GeometryEngine.Length(polyline);
Console.WriteLine($"折线长度:{length}");  // 15

// 计算多边形周长
double perimeter = GeometryEngine.Length(polygon);
Console.WriteLine($"多边形周长:{perimeter}");  // 40

5.4.4 面积计算:鞋带公式

public double Execute(Geometry geometry, SpatialReference? spatialRef = null)
{
    if (geometry is Polygon polygon)
    {
        double area = 0;
        foreach (var ring in polygon.GetRings())
        {
            var count = ring.Count;
            if (count < 3) continue;
            
            double ringArea = 0;
            for (var i = 0; i < count - 1; i++)
            {
                // 鞋带公式
                ringArea += ring[i].X * ring[i + 1].Y - ring[i + 1].X * ring[i].Y;
            }
            // 闭合环
            ringArea += ring[count - 1].X * ring[0].Y - ring[0].X * ring[count - 1].Y;
            area += Math.Abs(ringArea) * 0.5;
        }
        return area;
    }
    
    if (geometry is Envelope envelope)
    {
        return envelope.Width * envelope.Height;
    }
    
    return 0;  // 点和线没有面积
}

5.4.5 长度计算

public double Execute(Geometry geometry, SpatialReference? spatialRef = null)
{
    if (geometry is Polyline polyline)
    {
        double length = 0;
        foreach (var path in polyline.GetPaths())
        {
            for (var i = 0; i < path.Count - 1; i++)
            {
                length += path[i].Distance(path[i + 1]);
            }
        }
        return length;
    }
    
    if (geometry is Polygon polygon)
    {
        double length = 0;
        foreach (var ring in polygon.GetRings())
        {
            for (var i = 0; i < ring.Count - 1; i++)
            {
                length += ring[i].Distance(ring[i + 1]);
            }
            // 闭合段
            if (ring.Count > 1)
            {
                length += ring[ring.Count - 1].Distance(ring[0]);
            }
        }
        return length;
    }
    
    return 0;  // 点没有长度
}

5.5 Simplify(简化)

5.5.1 概念

使用 Douglas-Peucker 算法减少几何对象的顶点数量,同时保持其基本形状。常用于减少数据量,提高渲染性能。

应用场景:

  • 地图缩放时简化显示
  • 减少存储和传输的数据量
  • 提高地图渲染速度

5.5.2 API

public class SimplifyOperator : IGeometryOperator<Geometry>
{
    public static SimplifyOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, double tolerance, 
                           SpatialReference? spatialRef = null);
}

5.5.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 创建包含多个点的折线
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(1, 0.1),   // 几乎在直线上
    new Point(2, 0.05),  // 几乎在直线上
    new Point(3, 0.08),  // 几乎在直线上
    new Point(4, 0),
    new Point(5, 5),     // 明显转折
    new Point(6, 4.9),   // 几乎在直线上
    new Point(7, 5.1),   // 几乎在直线上
    new Point(8, 5),
    new Point(9, 0),
    new Point(10, 0)
});

Console.WriteLine($"原始点数:{polyline.GetPath(0).Count}");  // 11

// 使用容差 0.2 简化
var simplified = GeometryEngine.Simplify(polyline, 0.2);

if (simplified is Polyline simplifiedLine)
{
    Console.WriteLine($"简化后点数:{simplifiedLine.GetPath(0).Count}");  // 减少
}

// 使用更大的容差会更激进地简化
var moreSimplified = GeometryEngine.Simplify(polyline, 1.0);

5.5.4 实现原理:Douglas-Peucker 算法

private List<Point> DouglasPeucker(List<Point> points, double tolerance)
{
    if (points.Count < 3) return points;

    // 1. 找到距离首尾连线最远的点
    double maxDistance = 0;
    int maxIndex = 0;
    int end = points.Count - 1;

    for (int i = 1; i < end; i++)
    {
        double distance = PerpendicularDistance(points[i], points[0], points[end]);
        if (distance > maxDistance)
        {
            maxDistance = distance;
            maxIndex = i;
        }
    }

    // 2. 如果最大距离超过容差,递归处理两段
    if (maxDistance > tolerance)
    {
        var left = DouglasPeucker(points.GetRange(0, maxIndex + 1), tolerance);
        var right = DouglasPeucker(points.GetRange(maxIndex, end - maxIndex + 1), tolerance);

        // 合并结果(去掉重复的中间点)
        var result = new List<Point>(left);
        result.AddRange(right.Skip(1));
        return result;
    }

    // 3. 否则只保留端点
    return new List<Point> { points[0], points[end] };
}

// 计算点到线段的垂直距离
private double PerpendicularDistance(Point point, Point lineStart, Point lineEnd)
{
    double dx = lineEnd.X - lineStart.X;
    double dy = lineEnd.Y - lineStart.Y;
    double mag = Math.Sqrt(dx * dx + dy * dy);
    
    if (mag < GeometryConstants.Epsilon)
        return point.Distance(lineStart);

    // 参数化位置
    double u = ((point.X - lineStart.X) * dx + (point.Y - lineStart.Y) * dy) / (mag * mag);

    if (u < 0) return point.Distance(lineStart);
    if (u > 1) return point.Distance(lineEnd);

    // 最近点
    double ix = lineStart.X + u * dx;
    double iy = lineStart.Y + u * dy;
    return point.Distance(new Point(ix, iy));
}

算法复杂度:平均 O(n log n),最坏 O(n²)。

5.6 Centroid(质心)

5.6.1 概念

质心是几何对象的几何中心点(质量中心)。

5.6.2 API

public class CentroidOperator : IGeometryOperator<Point>
{
    public static CentroidOperator Instance { get; }
    
    public Point Execute(Geometry geometry, 
                        SpatialReference? spatialRef = null);
}

5.6.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 多边形的质心
var polygon = new Polygon();
polygon.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(10, 0),
    new Point(10, 10),
    new Point(0, 10),
    new Point(0, 0)
});

var centroid = GeometryEngine.Centroid(polygon);
Console.WriteLine($"质心:({centroid.X}, {centroid.Y})");  // (5, 5)

// 多点的质心(平均值)
var multiPoint = new MultiPoint();
multiPoint.Add(new Point(0, 0));
multiPoint.Add(new Point(10, 0));
multiPoint.Add(new Point(10, 10));
multiPoint.Add(new Point(0, 10));

var mpCentroid = GeometryEngine.Centroid(multiPoint);
Console.WriteLine($"多点质心:({mpCentroid.X}, {mpCentroid.Y})");  // (5, 5)

// 三角形的质心
var triangle = new Polygon();
triangle.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(6, 0),
    new Point(3, 6),
    new Point(0, 0)
});

var triangleCentroid = GeometryEngine.Centroid(triangle);
Console.WriteLine($"三角形质心:({triangleCentroid.X}, {triangleCentroid.Y})");  // (3, 2)

5.6.4 实现原理

public Point Execute(Geometry geometry, SpatialReference? spatialRef = null)
{
    if (geometry.IsEmpty)
        return new Point();

    switch (geometry.Type)
    {
        case GeometryType.Point:
            return (Point)geometry;

        case GeometryType.MultiPoint:
            return CalculateMultiPointCentroid((MultiPoint)geometry);

        case GeometryType.Polygon:
            return CalculatePolygonCentroid((Polygon)geometry);

        case GeometryType.Envelope:
            return ((Envelope)geometry).Center;

        default:
            // 对于其他类型,返回包络的中心
            return geometry.GetEnvelope().Center;
    }
}

private Point CalculateMultiPointCentroid(MultiPoint multiPoint)
{
    double sumX = 0, sumY = 0;
    int count = 0;

    foreach (var point in multiPoint.GetPoints())
    {
        sumX += point.X;
        sumY += point.Y;
        count++;
    }

    return new Point(sumX / count, sumY / count);
}

private Point CalculatePolygonCentroid(Polygon polygon)
{
    // 使用多边形质心公式
    double sumX = 0, sumY = 0, sumArea = 0;

    foreach (var ring in polygon.GetRings())
    {
        for (int i = 0; i < ring.Count - 1; i++)
        {
            double cross = ring[i].X * ring[i + 1].Y - ring[i + 1].X * ring[i].Y;
            sumX += (ring[i].X + ring[i + 1].X) * cross;
            sumY += (ring[i].Y + ring[i + 1].Y) * cross;
            sumArea += cross;
        }
    }

    double area = sumArea / 2;
    return new Point(sumX / (6 * area), sumY / (6 * area));
}

5.7 Boundary(边界)

5.7.1 概念

边界操作返回几何对象的边界几何。根据 OGC 规范:

  • 点的边界是空
  • 线的边界是端点
  • 多边形的边界是其环

5.7.2 API

public class BoundaryOperator : IGeometryOperator<Geometry>
{
    public static BoundaryOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, 
                           SpatialReference? spatialRef = null);
}

5.7.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 点的边界是空
var point = new Point(10, 20);
var pointBoundary = GeometryEngine.Boundary(point);
Console.WriteLine($"点边界为空:{pointBoundary.IsEmpty}");  // true

// 折线的边界是端点
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(10, 10),
    new Point(20, 0)
});

var lineBoundary = GeometryEngine.Boundary(polyline);
if (lineBoundary is MultiPoint boundaryPoints)
{
    Console.WriteLine($"折线边界点数:{boundaryPoints.Count}");  // 2(起点和终点)
}

// 多边形的边界是折线
var polygon = new Polygon();
polygon.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(10, 0),
    new Point(10, 10),
    new Point(0, 10),
    new Point(0, 0)
});

var polyBoundary = GeometryEngine.Boundary(polygon);
if (polyBoundary is Polyline boundaryLine)
{
    Console.WriteLine($"多边形边界长度:{boundaryLine.Length}");  // 40
}

5.8 Generalize(概化)

5.8.1 概念

概化操作通过删除顶点来简化几何对象,同时保持其总体形状。与 Simplify 类似,但参数含义不同:

  • Simplify:tolerance 是点到简化线的最大距离
  • Generalize:maxDeviation 是允许的最大偏差

5.8.2 API

public class GeneralizeOperator : IGeometryOperator<Geometry>
{
    public static GeneralizeOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, double maxDeviation, 
                           SpatialReference? spatialRef = null);
}

5.8.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(1, 0.5),
    new Point(2, 0),
    new Point(3, 0.3),
    new Point(4, 0),
    new Point(5, 1),
    new Point(6, 0)
});

Console.WriteLine($"原始点数:{polyline.GetPath(0).Count}");  // 7

var generalized = GeometryEngine.Generalize(polyline, 0.6);
if (generalized is Polyline genLine)
{
    Console.WriteLine($"概化后点数:{genLine.GetPath(0).Count}");  // 减少
}

5.9 Densify(密化)

5.9.1 概念

密化操作在几何对象的线段上添加额外的顶点,确保没有线段超过指定的最大长度。这是概化的逆操作。

应用场景:

  • 为投影转换准备几何
  • 沿曲线均匀采样
  • 提高空间分析精度

5.9.2 API

public class DensifyOperator : IGeometryOperator<Geometry>
{
    public static DensifyOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, double maxSegmentLength, 
                           SpatialReference? spatialRef = null);
}

5.9.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 简单的两点折线
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(0, 0),
    new Point(10, 0)  // 长度为 10 的线段
});

Console.WriteLine($"原始点数:{polyline.GetPath(0).Count}");  // 2

// 密化,最大线段长度为 2
var densified = GeometryEngine.Densify(polyline, 2);

if (densified is Polyline denLine)
{
    Console.WriteLine($"密化后点数:{denLine.GetPath(0).Count}");  // 6
    // 点:(0,0), (2,0), (4,0), (6,0), (8,0), (10,0)
}

// 多边形密化
var square = new Polygon();
square.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(10, 0),
    new Point(10, 10),
    new Point(0, 10),
    new Point(0, 0)
});

var densifiedSquare = GeometryEngine.Densify(square, 3);

5.9.4 实现原理

private List<Point> DensifyPath(IReadOnlyList<Point> path, double maxLength)
{
    var result = new List<Point>();

    for (int i = 0; i < path.Count - 1; i++)
    {
        result.Add(path[i]);
        
        var start = path[i];
        var end = path[i + 1];
        double length = start.Distance(end);

        if (length > maxLength)
        {
            // 计算需要插入的点数
            int numSegments = (int)Math.Ceiling(length / maxLength);
            
            for (int j = 1; j < numSegments; j++)
            {
                double t = (double)j / numSegments;
                result.Add(new Point(
                    start.X + t * (end.X - start.X),
                    start.Y + t * (end.Y - start.Y)
                ));
            }
        }
    }

    result.Add(path[path.Count - 1]);
    return result;
}

5.10 Clip(裁剪)

5.10.1 概念

裁剪操作将几何对象裁剪到指定的包络矩形范围内。常用于视口裁剪和数据范围限制。

5.10.2 API

public class ClipOperator : IGeometryOperator<Geometry>
{
    public static ClipOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, Envelope clipEnvelope);
}

5.10.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 创建裁剪范围
var clipEnvelope = new Envelope(0, 0, 50, 50);

// 裁剪点
var pointInside = new Point(25, 25);
var pointOutside = new Point(100, 100);

var clippedIn = GeometryEngine.Clip(pointInside, clipEnvelope);   // 返回原点
var clippedOut = GeometryEngine.Clip(pointOutside, clipEnvelope); // 返回空点

// 裁剪折线(Cohen-Sutherland 算法)
var polyline = new Polyline();
polyline.AddPath(new List<Point>
{
    new Point(-10, 25),  // 裁剪区外
    new Point(25, 25),   // 裁剪区内
    new Point(75, 25)    // 裁剪区外
});

var clippedLine = GeometryEngine.Clip(polyline, clipEnvelope);
// 结果:从 (0, 25) 到 (50, 25) 的线段

5.10.4 实现原理:Cohen-Sutherland 算法

// 区域码定义
private const int INSIDE = 0;  // 0000
private const int LEFT = 1;    // 0001
private const int RIGHT = 2;   // 0010
private const int BOTTOM = 4;  // 0100
private const int TOP = 8;     // 1000

private int ComputeOutCode(double x, double y, Envelope clip)
{
    int code = INSIDE;
    
    if (x < clip.XMin) code |= LEFT;
    else if (x > clip.XMax) code |= RIGHT;
    
    if (y < clip.YMin) code |= BOTTOM;
    else if (y > clip.YMax) code |= TOP;
    
    return code;
}

private Line? ClipLine(Point p1, Point p2, Envelope clip)
{
    int outcode1 = ComputeOutCode(p1.X, p1.Y, clip);
    int outcode2 = ComputeOutCode(p2.X, p2.Y, clip);
    
    double x1 = p1.X, y1 = p1.Y;
    double x2 = p2.X, y2 = p2.Y;
    
    while (true)
    {
        if ((outcode1 | outcode2) == 0)
        {
            // 两点都在内部
            return new Line(new Point(x1, y1), new Point(x2, y2));
        }
        else if ((outcode1 & outcode2) != 0)
        {
            // 线段完全在外部
            return null;
        }
        else
        {
            // 需要裁剪
            int outcodeOut = outcode1 != 0 ? outcode1 : outcode2;
            double x, y;
            
            if ((outcodeOut & TOP) != 0)
            {
                x = x1 + (x2 - x1) * (clip.YMax - y1) / (y2 - y1);
                y = clip.YMax;
            }
            else if ((outcodeOut & BOTTOM) != 0)
            {
                x = x1 + (x2 - x1) * (clip.YMin - y1) / (y2 - y1);
                y = clip.YMin;
            }
            else if ((outcodeOut & RIGHT) != 0)
            {
                y = y1 + (y2 - y1) * (clip.XMax - x1) / (x2 - x1);
                x = clip.XMax;
            }
            else
            {
                y = y1 + (y2 - y1) * (clip.XMin - x1) / (x2 - x1);
                x = clip.XMin;
            }
            
            // 更新端点
            if (outcodeOut == outcode1)
            {
                x1 = x; y1 = y;
                outcode1 = ComputeOutCode(x1, y1, clip);
            }
            else
            {
                x2 = x; y2 = y;
                outcode2 = ComputeOutCode(x2, y2, clip);
            }
        }
    }
}

5.11 Offset(偏移)

5.11.1 概念

偏移操作创建几何对象的偏移版本。正值向外偏移,负值向内偏移。

5.11.2 API

public class OffsetOperator : IGeometryOperator<Geometry>
{
    public static OffsetOperator Instance { get; }
    
    public Geometry Execute(Geometry geometry, double distance, 
                           SpatialReference? spatialRef = null);
}

5.11.3 使用示例

using OpenGIS.Esri.Geometry.Core;
using OpenGIS.Esri.Geometry.Core.Geometries;

// 多边形偏移
var polygon = new Polygon();
polygon.AddRing(new List<Point>
{
    new Point(0, 0),
    new Point(100, 0),
    new Point(100, 100),
    new Point(0, 100),
    new Point(0, 0)
});

// 向外偏移 10
var offsetPolygon = GeometryEngine.Offset(polygon, 10);

// 向内偏移 10
var insetPolygon = GeometryEngine.Offset(polygon, -10);

5.12 小结

本章详细介绍了 geometry-api-net 的几何运算操作符:

操作符 功能 核心算法
Buffer 创建缓冲区 几何扩展
ConvexHull 计算凸包 Graham 扫描
Area 计算面积 鞋带公式
Length 计算长度 欧几里得距离
Simplify 简化几何 Douglas-Peucker
Centroid 计算质心 多边形质心公式
Boundary 获取边界 OGC 规范
Generalize 概化几何 Douglas-Peucker 变体
Densify 密化几何 线段插值
Clip 裁剪几何 Cohen-Sutherland
Offset 偏移几何 几何偏移

这些操作符是进行空间分析的核心工具。在下一章中,我们将学习集合操作符,用于计算几何对象之间的并集、交集和差集。