znlgis 博客

GIS开发与技术分享

第20章:实际应用与最佳实践

20.1 概述

本章总结 Clipper2 在实际项目中的应用技巧、性能优化方法和常见问题的解决方案。

20.2 选择正确的类

20.2.1 类选择指南

需求 推荐类
整数坐标布尔运算 Clipper64
浮点坐标布尔运算 ClipperD
快速矩形裁剪 RectClip64
路径偏移 ClipperOffset
简单操作 静态方法 Clipper.*

20.2.2 静态方法 vs 类实例

// 简单操作用静态方法
Paths64 result = Clipper.Intersect(subjects, clips, FillRule.NonZero);

// 复杂操作用类实例
Clipper64 clipper = new Clipper64();
clipper.PreserveCollinear = true;
clipper.AddSubject(subjects);
clipper.AddClip(clips);
clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);

20.3 性能优化

20.3.1 重用 Clipper 实例

// 不好:每次创建新实例
foreach (var item in items)
{
    Clipper64 clipper = new Clipper64();  // 开销
    clipper.AddSubject(item.Subject);
    clipper.AddClip(item.Clip);
    clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}

// 好:重用实例
Clipper64 clipper = new Clipper64();
foreach (var item in items)
{
    clipper.Clear();  // 清除数据,保留配置
    clipper.AddSubject(item.Subject);
    clipper.AddClip(item.Clip);
    clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}

20.3.2 预分配容量

// 预估结果大小
int estimatedCount = subjects.Sum(p => p.Count);
Paths64 result = new Paths64(estimatedCount);

20.3.3 使用边界框过滤

// 快速跳过不相交的多边形
Rect64 clipBounds = Clipper.GetBounds(clips);

foreach (Path64 subject in subjects)
{
    Rect64 subjectBounds = Clipper.GetBounds(subject);
    
    if (!subjectBounds.Intersects(clipBounds))
    {
        // 完全不相交,跳过
        continue;
    }
    
    // 执行裁剪
    clipper.AddSubject(subject);
}

20.3.4 并行处理

// 对于大量独立的裁剪操作
var results = items.AsParallel().Select(item =>
{
    // 每个线程有自己的 Clipper 实例
    Clipper64 clipper = new Clipper64();
    clipper.AddSubject(item.Subject);
    clipper.AddClip(item.Clip);
    
    Paths64 result = new Paths64();
    clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
    return result;
}).ToList();

20.4 精度处理

20.4.1 选择合适的精度

// 屏幕坐标(像素)
ClipperD clipper = new ClipperD(0);  // 整数精度

// CAD 坐标(毫米,保留 2 位小数)
ClipperD clipper = new ClipperD(2);  // 0.01 精度

// 地理坐标(经纬度,保留 6 位小数)
ClipperD clipper = new ClipperD(6);  // 0.000001 精度

20.4.2 坐标缩放

// 手动缩放可以获得更好的控制
double scale = 1000.0;  // 保留 3 位小数

// 输入时缩放
Path64 scaledSubject = ScalePath(subject, scale);
Path64 scaledClip = ScalePath(clip, scale);

// 处理
Clipper64 clipper = new Clipper64();
clipper.AddSubject(scaledSubject);
clipper.AddClip(scaledClip);
Paths64 scaledResult = new Paths64();
clipper.Execute(ClipType.Intersection, FillRule.NonZero, scaledResult);

// 输出时还原
PathsD result = ScalePaths(scaledResult, 1.0 / scale);

20.4.3 避免坐标溢出

// 检查坐标范围
const long MaxCoord = InternalClipper.MaxCoord;

bool IsValidPath(Path64 path)
{
    foreach (var pt in path)
    {
        if (Math.Abs(pt.X) > MaxCoord || Math.Abs(pt.Y) > MaxCoord)
            return false;
    }
    return true;
}

// 必要时缩小坐标
PathD NormalizePath(PathD path, double maxRange)
{
    Rect64 bounds = Clipper.GetBounds(Clipper.ScalePath64(path, 1.0));
    double scale = maxRange / Math.Max(
        Math.Max(bounds.Width, bounds.Height), 1);
    
    return ScalePath(path, scale);
}

20.5 错误处理

20.5.1 检查输入有效性

bool IsValidPolygon(Path64 path)
{
    // 至少 3 个点
    if (path.Count < 3) return false;
    
    // 检查面积
    if (Math.Abs(Clipper.Area(path)) < 1.0) return false;
    
    // 检查退化边
    for (int i = 0; i < path.Count; i++)
    {
        int j = (i + 1) % path.Count;
        if (path[i] == path[j]) return false;
    }
    
    return true;
}

20.5.2 处理空结果

Paths64 result = new Paths64();
bool success = clipper.Execute(ClipType.Intersection, 
    FillRule.NonZero, result);

if (!success)
{
    Console.WriteLine("裁剪操作失败");
    return null;
}

if (result.Count == 0)
{
    Console.WriteLine("结果为空(可能完全不相交)");
    return result;
}

20.5.3 异常处理

try
{
    clipper.AddSubject(subject);
    clipper.AddClip(clip);
    clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}
catch (Exception ex)
{
    // 记录错误
    Console.WriteLine($"裁剪错误: {ex.Message}");
    
    // 尝试简化后重试
    subject = Clipper.SimplifyPath(subject, 1.0);
    clip = Clipper.SimplifyPath(clip, 1.0);
    
    clipper.Clear();
    clipper.AddSubject(subject);
    clipper.AddClip(clip);
    clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
}

20.6 常见应用场景

20.6.1 地图瓦片切割

public Paths64 CutToTile(Paths64 geometry, int tileX, int tileY, int zoom)
{
    // 计算瓦片边界
    double tileSize = 360.0 / Math.Pow(2, zoom);
    double left = -180.0 + tileX * tileSize;
    double bottom = -90.0 + tileY * tileSize;
    
    // 使用 RectClip 快速裁剪
    Rect64 tileBounds = new Rect64(
        (long)(left * 1000000),
        (long)(bottom * 1000000),
        (long)((left + tileSize) * 1000000),
        (long)((bottom + tileSize) * 1000000)
    );
    
    RectClip64 rc = new RectClip64(tileBounds);
    return rc.Execute(geometry);
}

20.6.2 缓冲区分析

public Paths64 CreateBuffer(Path64 path, double distance, 
    bool isPolygon)
{
    ClipperOffset co = new ClipperOffset();
    
    // 配置参数
    co.ArcTolerance = distance * 0.01;  // 1% 精度
    
    EndType endType = isPolygon ? EndType.Polygon : EndType.Round;
    co.AddPath(path, JoinType.Round, endType);
    
    Paths64 result = new Paths64();
    co.Execute(distance, result);
    
    return result;
}

20.6.3 多边形简化

public Path64 SimplifyPolygon(Path64 path, double tolerance)
{
    // Douglas-Peucker 简化
    Path64 simplified = Clipper.SimplifyPath(path, tolerance);
    
    // 清理自相交
    Paths64 clean = Clipper.SimplifyPaths(new Paths64 { simplified }, 
        tolerance);
    
    if (clean.Count > 0)
        return clean[0];
    
    return path;
}

20.6.4 布尔运算组合

public Paths64 ComplexOperation(Paths64 baseShape, 
    Paths64 additions, Paths64 subtractions)
{
    Clipper64 clipper = new Clipper64();
    
    // 先合并基础形状和添加部分
    clipper.AddSubject(baseShape);
    clipper.AddClip(additions);
    
    Paths64 unionResult = new Paths64();
    clipper.Execute(ClipType.Union, FillRule.NonZero, unionResult);
    
    // 再减去要删除的部分
    clipper.Clear();
    clipper.AddSubject(unionResult);
    clipper.AddClip(subtractions);
    
    Paths64 finalResult = new Paths64();
    clipper.Execute(ClipType.Difference, FillRule.NonZero, finalResult);
    
    return finalResult;
}

20.6.5 路径膨胀/收缩

public Paths64 CreateOutline(Path64 shape, double outlineWidth)
{
    ClipperOffset co = new ClipperOffset();
    co.AddPath(shape, JoinType.Miter, EndType.Polygon);
    
    // 外边界
    Paths64 outer = new Paths64();
    co.Execute(outlineWidth / 2, outer);
    
    // 内边界
    co.Clear();
    co.AddPath(shape, JoinType.Miter, EndType.Polygon);
    Paths64 inner = new Paths64();
    co.Execute(-outlineWidth / 2, inner);
    
    // 相减得到轮廓
    return Clipper.Difference(outer, inner, FillRule.NonZero);
}

20.7 调试技巧

20.7.1 可视化输出

public void SaveToSvg(Paths64 paths, string filename)
{
    Rect64 bounds = Clipper.GetBounds(paths);
    
    using (StreamWriter sw = new StreamWriter(filename))
    {
        sw.WriteLine($"<svg viewBox=\"{bounds.left} {bounds.top} " +
            $"{bounds.Width} {bounds.Height}\" " +
            $"xmlns=\"http://www.w3.org/2000/svg\">");
        
        foreach (Path64 path in paths)
        {
            sw.Write("<polygon points=\"");
            foreach (Point64 pt in path)
            {
                sw.Write($"{pt.X},{pt.Y} ");
            }
            sw.WriteLine("\" fill=\"blue\" stroke=\"black\" />");
        }
        
        sw.WriteLine("</svg>");
    }
}

20.7.2 日志记录

public void LogOperation(string operation, Paths64 subject, 
    Paths64 clip, Paths64 result)
{
    Console.WriteLine($"=== {operation} ===");
    Console.WriteLine($"Subject: {subject.Count} paths, " +
        $"{subject.Sum(p => p.Count)} points");
    Console.WriteLine($"Clip: {clip.Count} paths, " +
        $"{clip.Sum(p => p.Count)} points");
    Console.WriteLine($"Result: {result.Count} paths, " +
        $"{result.Sum(p => p.Count)} points");
    Console.WriteLine($"Area: {result.Sum(p => Math.Abs(Clipper.Area(p)))}");
}

20.7.3 边界情况测试

[Test]
public void TestEdgeCases()
{
    // 空输入
    Assert.DoesNotThrow(() => 
        Clipper.Intersect(new Paths64(), new Paths64(), FillRule.NonZero));
    
    // 单点
    var singlePoint = new Paths64 { new Path64 { new Point64(0, 0) } };
    Assert.DoesNotThrow(() => 
        Clipper.Union(singlePoint, FillRule.NonZero));
    
    // 共线点
    var collinear = new Path64 { 
        new Point64(0, 0), 
        new Point64(50, 0), 
        new Point64(100, 0) 
    };
    Assert.DoesNotThrow(() => 
        Clipper.SimplifyPath(collinear, 0.0));
}

20.8 内存管理

20.8.1 及时清理

// 大批量处理时,定期调用 GC
int batchSize = 1000;
int processed = 0;

foreach (var item in items)
{
    ProcessItem(item);
    processed++;
    
    if (processed % batchSize == 0)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

20.8.2 避免内存泄漏

// 使用 using 或 try-finally 确保清理
void ProcessWithCleanup()
{
    Clipper64 clipper = new Clipper64();
    Paths64 result = new Paths64();
    
    try
    {
        clipper.AddSubject(subjects);
        clipper.AddClip(clips);
        clipper.Execute(ClipType.Intersection, FillRule.NonZero, result);
    }
    finally
    {
        clipper.Clear();
        // 如果不再需要 result,也清理
    }
}

20.9 最佳实践清单

20.9.1 输入验证

  • 检查路径点数(闭合路径 ≥ 3,开放路径 ≥ 2)
  • 检查坐标范围不超过 MaxCoord
  • 移除重复点
  • 确保路径方向一致

20.9.2 性能优化

  • 重用 Clipper 实例
  • 使用边界框预过滤
  • 对矩形裁剪使用 RectClip64
  • 考虑并行处理

20.9.3 精度控制

  • 选择合适的精度级别
  • 理解整数与浮点的转换
  • 避免精度丢失

20.9.4 错误处理

  • 检查 Execute 返回值
  • 处理空结果
  • 捕获并记录异常

20.10 本章小结

使用 Clipper2 的最佳实践:

  1. 选择正确的类:根据需求选择 Clipper64、ClipperD 或 RectClip64
  2. 性能优化:重用实例、预过滤、并行处理
  3. 精度处理:选择合适精度、避免溢出
  4. 错误处理:验证输入、处理异常、检查结果
  5. 内存管理:及时清理、避免泄漏

遵循这些最佳实践可以帮助你在项目中高效、正确地使用 Clipper2。


上一章:PolyTree多边形树结构 返回目录

教程总结

本教程全面介绍了 Clipper2 C# 源代码的各个方面:

  1. 基础篇(第1-5章):核心数据结构、路径表示、枚举类型
  2. 核心架构篇(第6-10章):内部工具类、高精度运算、扫描线算法核心结构
  3. 输出结构篇(第11-13章):输出多边形构建、Clipper64 和 ClipperD 类
  4. 布尔运算篇(第14-15章):执行流程、填充规则
  5. 高级功能篇(第16-18章):偏移、矩形裁剪、Minkowski 运算
  6. 进阶篇(第19-20章):PolyTree 结构、实际应用

希望本教程能帮助你深入理解 Clipper2 的实现原理,在实际项目中更好地应用这个优秀的几何库。