znlgis 博客

GIS开发与技术分享

第六章 高级应用与性能优化(C#版)

6.1 引言

在前面的章节中,我们学习了Clipper2的核心功能:布尔运算、多边形偏移、矩形裁剪和闵可夫斯基运算。本章将深入探讨Clipper2 C#版本的高级应用技巧、性能优化方法以及与其他.NET工具和系统的集成方式。通过本章的学习,您将能够在实际.NET项目中更加高效地使用Clipper2。

6.2 Z轴值支持(USINGZ)

6.2.1 启用Z轴支持

Clipper2支持在每个顶点上附加一个Z轴值。这个功能需要通过编译选项启用:

C++

// 在包含头文件之前定义宏
#define CLIPPER2_USINGZ
#include "clipper2/clipper.h"

或者在CMake中启用:

add_compile_definitions(CLIPPER2_USINGZ)

C#

在C#版本中,需要添加USINGZ程序集:

using Clipper2Lib;
using Clipper2Lib.USINGZ;  // 添加Z值支持

6.2.2 Z值的用途

Z值可以用于多种目的:

存储顶点标识

// 为每个顶点分配唯一ID
Path64 path;
for (int i = 0; i < points.size(); i++) {
    path.push_back(Point64(points[i].x, points[i].y, i));  // z = 顶点索引
}

存储高程数据

// 在GIS应用中存储高程
Path64 contour;
for (const auto& pt : terrainPoints) {
    contour.push_back(Point64(
        static_cast<int64_t>(pt.x * 1000),
        static_cast<int64_t>(pt.y * 1000),
        static_cast<int64_t>(pt.elevation * 1000)
    ));
}

存储自定义属性

// 存储颜色索引或材质ID
Path64 polygon;
for (const auto& vertex : vertices) {
    polygon.push_back(Point64(
        vertex.x, vertex.y,
        vertex.materialId  // 材质ID作为Z值
    ));
}

6.2.3 Z值回调函数

当Clipper2执行布尔运算时,可能会产生新的顶点(在两边相交处)。通过设置回调函数,可以控制这些新顶点的Z值如何计算:

#define CLIPPER2_USINGZ
#include "clipper2/clipper.h"

using namespace Clipper2Lib;

// Z值回调函数
void ZCallback(const Point64& e1bot, const Point64& e1top,
               const Point64& e2bot, const Point64& e2top,
               Point64& pt) {
    // 使用线性插值计算新顶点的Z值
    
    // 计算交点在edge1上的位置比例
    double t1 = 0.5;  // 简化处理,实际应该根据交点位置计算
    
    // 插值Z值
    int64_t z1 = e1bot.z + static_cast<int64_t>((e1top.z - e1bot.z) * t1);
    int64_t z2 = e2bot.z + static_cast<int64_t>((e2top.z - e2bot.z) * 0.5);
    
    // 使用平均值
    pt.z = (z1 + z2) / 2;
}

int main() {
    Clipper64 clipper;
    
    // 设置Z值回调
    clipper.SetZCallback(ZCallback);
    
    // 添加多边形(带Z值)
    Paths64 subject;
    Path64 path;
    path.push_back(Point64(0, 0, 100));
    path.push_back(Point64(100, 0, 200));
    path.push_back(Point64(100, 100, 300));
    path.push_back(Point64(0, 100, 400));
    subject.push_back(path);
    
    clipper.AddSubject(subject);
    clipper.AddClip(clipPaths);
    
    Paths64 result;
    clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
    
    // result中的顶点包含计算后的Z值
    return 0;
}

6.2.4 C#中的Z值回调

using Clipper2Lib;

class Program
{
    // Z值回调委托
    static void MyZCallback(Point64 e1bot, Point64 e1top,
                            Point64 e2bot, Point64 e2top,
                            ref Point64 pt)
    {
        // 计算新顶点的Z值
        pt.Z = (e1bot.Z + e1top.Z + e2bot.Z + e2top.Z) / 4;
    }
    
    static void Main()
    {
        Clipper64 clipper = new Clipper64();
        clipper.ZCallback = MyZCallback;
        
        // ... 执行运算
    }
}

6.3 输出格式控制

6.3.1 PolyTree与Paths的选择

Clipper2支持两种输出格式:

Paths输出

Clipper64 clipper;
clipper.AddSubject(subject);
clipper.AddClip(clip);

Paths64 result;
clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
// result是一个扁平的路径列表,不包含层次信息

PolyTree输出

Clipper64 clipper;
clipper.AddSubject(subject);
clipper.AddClip(clip);

PolyTree64 tree;
Paths64 openPaths;  // 开放路径输出
clipper.Execute(ClipType::Intersection, FillRule::NonZero, tree, openPaths);
// tree包含完整的层次信息

选择建议

场景 推荐格式
简单的多边形处理 Paths
需要区分外边界和孔洞 PolyTree
后续需要进行嵌套分析 PolyTree
性能敏感的场景 Paths(略快)
需要遍历层次结构 PolyTree

6.3.2 保留共线点

默认情况下,Clipper2会移除共线的点(位于同一直线上的中间点)。可以通过设置选项保留这些点:

Clipper64 clipper;
clipper.PreserveCollinear(true);  // 保留共线点

clipper.AddSubject(subject);
Paths64 result;
clipper.Execute(ClipType::Union, FillRule::NonZero, result);

应用场景

6.3.3 反转输出方向

可以设置输出多边形的方向反转:

Clipper64 clipper;
clipper.ReverseSolution(true);  // 反转输出方向

// 原本逆时针的外边界会变成顺时针
// 原本顺时针的孔洞会变成逆时针

应用场景

6.4 错误处理与验证

6.4.1 输入验证

// 验证路径是否有效
bool ValidatePath(const Path64& path) {
    // 至少需要3个顶点
    if (path.size() < 3) {
        return false;
    }
    
    // 检查是否有重复的相邻顶点
    for (size_t i = 0; i < path.size(); i++) {
        if (path[i] == path[(i + 1) % path.size()]) {
            return false;
        }
    }
    
    // 检查面积是否为零
    double area = Area(path);
    if (std::abs(area) < 1.0) {
        return false;
    }
    
    return true;
}

// 验证所有路径
bool ValidatePaths(const Paths64& paths) {
    for (const auto& path : paths) {
        if (!ValidatePath(path)) {
            return false;
        }
    }
    return true;
}

6.4.2 结果验证

// 验证布尔运算结果
bool ValidateResult(const Paths64& result, ClipType clipType,
                    const Paths64& subject, const Paths64& clip) {
    if (result.empty()) {
        // 对于某些情况,空结果可能是正确的
        if (clipType == ClipType::Intersection) {
            // 如果没有相交,结果可以为空
            return true;
        }
    }
    
    // 检查结果是否有效
    for (const auto& path : result) {
        if (path.size() < 3) {
            return false;
        }
    }
    
    // 可以添加更多验证逻辑...
    return true;
}

6.4.3 异常处理

try {
    Clipper64 clipper;
    clipper.AddSubject(subject);
    clipper.AddClip(clip);
    
    Paths64 result;
    bool success = clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
    
    if (!success) {
        // 处理执行失败
        std::cerr << "Clipper执行失败" << std::endl;
    }
} catch (const std::exception& e) {
    std::cerr << "异常: " << e.what() << std::endl;
}

6.5 性能优化技巧

6.5.1 减少顶点数量

顶点数量是影响性能的主要因素。可以通过路径简化来减少顶点:

// 使用Douglas-Peucker算法简化路径
PathsD simplified = SimplifyPaths(paths, tolerance);
// tolerance值越大,简化越多,但形状变形也越大

// 或者使用Ramer-Douglas-Peucker变体
PathsD rdpSimplified = RamerDouglasPeucker(paths, tolerance);

选择合适的容差

// 根据应用场景选择容差
double tolerance;
if (isScreenRendering) {
    // 屏幕渲染:1像素以下的细节不可见
    tolerance = 1.0;
} else if (isCNC) {
    // CNC加工:保持0.01mm精度
    tolerance = 10;  // 假设单位是0.001mm
} else if (isGIS) {
    // GIS应用:根据地图比例尺选择
    tolerance = mapScale / 1000.0;
}

6.5.2 使用边界框预筛选

在执行布尔运算之前,使用边界框快速排除不可能相交的情况:

bool MayIntersect(const Paths64& paths1, const Paths64& paths2) {
    Rect64 bounds1 = Bounds(paths1);
    Rect64 bounds2 = Bounds(paths2);
    return bounds1.Intersects(bounds2);
}

// 使用预筛选
if (MayIntersect(subject, clip)) {
    Paths64 result = Intersect(subject, clip, FillRule::NonZero);
    // 处理结果
} else {
    // 不相交,跳过计算
}

6.5.3 批量操作优化

// 不优化的方式:每次创建新的Clipper对象
for (const auto& subject : subjects) {
    Clipper64 clipper;  // 每次创建新对象
    clipper.AddSubject(subject);
    clipper.AddClip(clip);
    Paths64 result;
    clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
}

// 优化的方式:复用Clipper对象
Clipper64 clipper;
for (const auto& subject : subjects) {
    clipper.Clear();  // 清空但保留内存分配
    clipper.AddSubject(subject);
    clipper.AddClip(clip);
    Paths64 result;
    clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
}

6.5.4 并行处理

#include <thread>
#include <future>
#include <vector>

std::vector<Paths64> ParallelProcess(
        const std::vector<Paths64>& subjects,
        const Paths64& clip,
        int numThreads) {
    
    std::vector<std::future<Paths64>> futures;
    std::vector<Paths64> results(subjects.size());
    
    // 启动异步任务
    for (size_t i = 0; i < subjects.size(); i++) {
        futures.push_back(std::async(std::launch::async, 
            [&subjects, &clip, i]() {
                // 每个线程有自己的Clipper实例
                Clipper64 clipper;
                clipper.AddSubject(subjects[i]);
                clipper.AddClip(clip);
                Paths64 result;
                clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
                return result;
            }
        ));
    }
    
    // 收集结果
    for (size_t i = 0; i < futures.size(); i++) {
        results[i] = futures[i].get();
    }
    
    return results;
}

6.5.5 内存优化

// 预分配内存
Paths64 subject;
subject.reserve(100);  // 预分配100个路径的空间

for (int i = 0; i < 100; i++) {
    Path64 path;
    path.reserve(1000);  // 每个路径预分配1000个顶点
    // ... 填充path
    subject.push_back(std::move(path));  // 使用移动语义
}

6.5.6 选择合适的数据类型

// 如果坐标范围较小,可以考虑使用较小的类型
// 但Clipper2默认使用int64_t以确保精度

// 对于浮点数,如果不需要高精度,可以降低精度
PathsD floatPaths = ...;
int precision = 2;  // 只保留2位小数
Paths64 intPaths = ConvertToInt64(floatPaths, std::pow(10, precision));

6.6 与其他库的集成

6.6.1 与OpenGL集成

#include <GL/gl.h>
#include "clipper2/clipper.h"

void RenderPaths(const Paths64& paths) {
    for (const auto& path : paths) {
        glBegin(GL_LINE_LOOP);
        for (const auto& pt : path) {
            glVertex2d(pt.x / 1000.0, pt.y / 1000.0);
        }
        glEnd();
    }
}

void RenderFilledPaths(const Paths64& paths) {
    // 使用三角化(需要额外的三角化库)
    std::vector<Triangle> triangles = Triangulate(paths);
    
    glBegin(GL_TRIANGLES);
    for (const auto& tri : triangles) {
        glVertex2d(tri.a.x / 1000.0, tri.a.y / 1000.0);
        glVertex2d(tri.b.x / 1000.0, tri.b.y / 1000.0);
        glVertex2d(tri.c.x / 1000.0, tri.c.y / 1000.0);
    }
    glEnd();
}

6.6.2 与SVG集成

Clipper2提供了SVG辅助工具:

#include "clipper2/clipper.h"
#include "utils/clipper.svg.utils.h"

void SaveToSVG(const Paths64& paths, const std::string& filename) {
    SvgWriter writer;
    
    writer.AddPaths(paths, true,   // 闭合路径
                    FillRule::NonZero,
                    0x1000AA00,    // 填充颜色
                    0xFF009900,    // 描边颜色
                    1);            // 描边宽度
    
    writer.SaveToFile(filename, 800, 600);  // 800x600的SVG
}

6.6.3 与GeoJSON集成

#include <nlohmann/json.hpp>

using json = nlohmann::json;

// 将Paths转换为GeoJSON
json PathsToGeoJSON(const Paths64& paths, double scale = 1000.0) {
    json features = json::array();
    
    for (const auto& path : paths) {
        json coordinates = json::array();
        
        for (const auto& pt : path) {
            coordinates.push_back({pt.x / scale, pt.y / scale});
        }
        // 闭合多边形
        coordinates.push_back({path[0].x / scale, path[0].y / scale});
        
        json feature = {
            {"type", "Feature"},
            {"geometry", {
                {"type", "Polygon"},
                {"coordinates", json::array({coordinates})}
            }},
            {"properties", json::object()}
        };
        
        features.push_back(feature);
    }
    
    return {
        {"type", "FeatureCollection"},
        {"features", features}
    };
}

// 从GeoJSON读取Paths
Paths64 GeoJSONToPaths(const json& geojson, double scale = 1000.0) {
    Paths64 result;
    
    for (const auto& feature : geojson["features"]) {
        const auto& geometry = feature["geometry"];
        
        if (geometry["type"] == "Polygon") {
            for (const auto& ring : geometry["coordinates"]) {
                Path64 path;
                for (size_t i = 0; i < ring.size() - 1; i++) {  // 跳过闭合点
                    path.push_back(Point64(
                        static_cast<int64_t>(ring[i][0].get<double>() * scale),
                        static_cast<int64_t>(ring[i][1].get<double>() * scale)
                    ));
                }
                result.push_back(path);
            }
        }
    }
    
    return result;
}

6.6.4 与GDAL/OGR集成

#include <ogrsf_frmts.h>
#include "clipper2/clipper.h"

// 从OGR几何体转换
Paths64 OGRGeometryToPaths(OGRGeometry* geom, double scale = 1000.0) {
    Paths64 result;
    
    if (geom->getGeometryType() == wkbPolygon) {
        OGRPolygon* polygon = (OGRPolygon*)geom;
        
        // 外环
        OGRLinearRing* exteriorRing = polygon->getExteriorRing();
        Path64 exterior;
        for (int i = 0; i < exteriorRing->getNumPoints() - 1; i++) {
            exterior.push_back(Point64(
                static_cast<int64_t>(exteriorRing->getX(i) * scale),
                static_cast<int64_t>(exteriorRing->getY(i) * scale)
            ));
        }
        result.push_back(exterior);
        
        // 内环(孔洞)
        for (int r = 0; r < polygon->getNumInteriorRings(); r++) {
            OGRLinearRing* ring = polygon->getInteriorRing(r);
            Path64 hole;
            for (int i = 0; i < ring->getNumPoints() - 1; i++) {
                hole.push_back(Point64(
                    static_cast<int64_t>(ring->getX(i) * scale),
                    static_cast<int64_t>(ring->getY(i) * scale)
                ));
            }
            result.push_back(hole);
        }
    }
    
    return result;
}

// 转换回OGR几何体
OGRGeometry* PathsToOGRGeometry(const Paths64& paths, double scale = 1000.0) {
    if (paths.empty()) return nullptr;
    
    OGRPolygon* polygon = new OGRPolygon();
    
    for (size_t i = 0; i < paths.size(); i++) {
        const auto& path = paths[i];
        OGRLinearRing* ring = new OGRLinearRing();
        
        for (const auto& pt : path) {
            ring->addPoint(pt.x / scale, pt.y / scale);
        }
        ring->closeRings();
        
        if (i == 0) {
            polygon->addRingDirectly(ring);
        } else {
            polygon->addRingDirectly(ring);
        }
    }
    
    return polygon;
}

6.7 调试与可视化

6.7.1 使用SVG进行调试

void DebugVisualize(const Paths64& subject,
                    const Paths64& clip,
                    const Paths64& result,
                    const std::string& filename) {
    SvgWriter svg;
    
    // 绘制主体(半透明蓝色)
    svg.AddPaths(subject, true, FillRule::NonZero,
                 0x200000FF, 0xFF0000FF, 2);
    
    // 绘制裁剪区域(半透明红色)
    svg.AddPaths(clip, true, FillRule::NonZero,
                 0x20FF0000, 0xFFFF0000, 2);
    
    // 绘制结果(半透明绿色)
    svg.AddPaths(result, true, FillRule::NonZero,
                 0x4000FF00, 0xFF00FF00, 3);
    
    svg.SaveToFile(filename, 800, 600);
}

6.7.2 打印路径信息

void PrintPathInfo(const Paths64& paths, const std::string& name) {
    std::cout << "===== " << name << " =====" << std::endl;
    std::cout << "路径数量: " << paths.size() << std::endl;
    
    size_t totalVertices = 0;
    for (const auto& path : paths) {
        totalVertices += path.size();
    }
    std::cout << "总顶点数: " << totalVertices << std::endl;
    
    double totalArea = Area(paths);
    std::cout << "总面积: " << totalArea << std::endl;
    
    Rect64 bounds = Bounds(paths);
    std::cout << "边界框: (" << bounds.left << ", " << bounds.top 
              << ") - (" << bounds.right << ", " << bounds.bottom << ")" << std::endl;
    
    for (size_t i = 0; i < paths.size(); i++) {
        const auto& path = paths[i];
        double area = Area(path);
        bool isHole = area < 0;
        std::cout << "  路径 " << i << ": " 
                  << path.size() << " 顶点, "
                  << "面积 " << std::abs(area)
                  << (isHole ? " (孔洞)" : " (外边界)")
                  << std::endl;
    }
}

6.7.3 性能分析

#include <chrono>
#include <iostream>

class Timer {
public:
    Timer(const std::string& name) : name_(name) {
        start_ = std::chrono::high_resolution_clock::now();
    }
    
    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start_);
        std::cout << name_ << ": " << duration.count() / 1000.0 << " ms" << std::endl;
    }
    
private:
    std::string name_;
    std::chrono::high_resolution_clock::time_point start_;
};

// 使用
void ProfileOperation() {
    Paths64 subject = ...;
    Paths64 clip = ...;
    
    {
        Timer t("交集运算");
        Paths64 result = Intersect(subject, clip, FillRule::NonZero);
    }
    
    {
        Timer t("偏移运算");
        Paths64 result = InflatePaths(subject, 10, JoinType::Round, EndType::Polygon);
    }
}

6.8 最佳实践

6.8.1 代码组织

// 封装Clipper2操作的工具类
class GeometryProcessor {
public:
    GeometryProcessor() : scale_(1000.0) {}
    
    // 设置精度
    void SetScale(double scale) { scale_ = scale; }
    
    // 布尔运算接口
    PathsD Intersect(const PathsD& subject, const PathsD& clip) {
        Paths64 subj64 = ConvertToInt(subject);
        Paths64 clip64 = ConvertToInt(clip);
        Paths64 result = Clipper2Lib::Intersect(subj64, clip64, FillRule::NonZero);
        return ConvertToDouble(result);
    }
    
    PathsD Union(const PathsD& paths) {
        Paths64 paths64 = ConvertToInt(paths);
        Paths64 result = Clipper2Lib::Union(paths64, FillRule::NonZero);
        return ConvertToDouble(result);
    }
    
    PathsD Offset(const PathsD& paths, double delta) {
        Paths64 paths64 = ConvertToInt(paths);
        int64_t delta64 = static_cast<int64_t>(delta * scale_);
        Paths64 result = InflatePaths(paths64, delta64, JoinType::Round, EndType::Polygon);
        return ConvertToDouble(result);
    }
    
private:
    double scale_;
    
    Paths64 ConvertToInt(const PathsD& paths) {
        return ScalePaths<int64_t, double>(paths, scale_);
    }
    
    PathsD ConvertToDouble(const Paths64& paths) {
        return ScalePaths<double, int64_t>(paths, 1.0 / scale_);
    }
};

6.8.2 错误处理策略

class ClipperException : public std::runtime_error {
public:
    ClipperException(const std::string& msg) : std::runtime_error(msg) {}
};

Paths64 SafeIntersect(const Paths64& subject, const Paths64& clip) {
    // 输入验证
    if (subject.empty()) {
        throw ClipperException("主体多边形为空");
    }
    if (clip.empty()) {
        throw ClipperException("裁剪多边形为空");
    }
    
    // 检查边界框是否相交
    Rect64 subjectBounds = Bounds(subject);
    Rect64 clipBounds = Bounds(clip);
    if (!subjectBounds.Intersects(clipBounds)) {
        return Paths64();  // 不相交,返回空
    }
    
    try {
        Clipper64 clipper;
        clipper.AddSubject(subject);
        clipper.AddClip(clip);
        
        Paths64 result;
        bool success = clipper.Execute(ClipType::Intersection, FillRule::NonZero, result);
        
        if (!success) {
            throw ClipperException("Clipper执行失败");
        }
        
        return result;
    } catch (const std::exception& e) {
        throw ClipperException(std::string("Clipper错误: ") + e.what());
    }
}

6.8.3 配置管理

struct ClipperConfig {
    double scale = 1000.0;
    FillRule fillRule = FillRule::NonZero;
    JoinType joinType = JoinType::Round;
    EndType endType = EndType::Polygon;
    double miterLimit = 2.0;
    double arcTolerance = 0.25;
    bool preserveCollinear = false;
    bool reverseSolution = false;
};

class ConfigurableClipper {
public:
    void SetConfig(const ClipperConfig& config) {
        config_ = config;
    }
    
    Paths64 Offset(const Paths64& paths, double delta) {
        ClipperOffset offsetter;
        offsetter.MiterLimit(config_.miterLimit);
        offsetter.ArcTolerance(config_.arcTolerance);
        offsetter.PreserveCollinear(config_.preserveCollinear);
        offsetter.ReverseSolution(config_.reverseSolution);
        
        offsetter.AddPaths(paths, config_.joinType, config_.endType);
        
        Paths64 result;
        offsetter.Execute(delta * config_.scale, result);
        return result;
    }
    
private:
    ClipperConfig config_;
};

6.9 常见陷阱与解决方案

6.9.1 坐标范围溢出

// 错误:坐标过大可能导致溢出
Path64 badPath = MakePath({
    1e18, 0,
    1e18, 1e18,
    0, 1e18
});

// 正确:使用合理的缩放
const double scale = 1000000.0;  // 6位小数精度
Path64 goodPath = MakePath({
    static_cast<int64_t>(1e12 * scale), 0,
    static_cast<int64_t>(1e12 * scale), static_cast<int64_t>(1e12 * scale),
    0, static_cast<int64_t>(1e12 * scale)
});

6.9.2 填充规则不匹配

// 问题:使用不匹配的填充规则
Paths64 paths;
paths.push_back(MakePath({0, 0, 100, 0, 100, 100, 0, 100}));  // 逆时针
paths.push_back(MakePath({25, 25, 25, 75, 75, 75, 75, 25}));  // 顺时针孔洞

// 使用EvenOdd规则可能不会正确识别孔洞
// Paths64 result = Union(paths, FillRule::EvenOdd);

// 正确:使用NonZero规则并确保方向正确
Paths64 result = Union(paths, FillRule::NonZero);

6.9.3 精度损失

// 问题:浮点数精度不足
double x = 1.23456789012345;  // 精度可能丢失

// 解决:使用足够的缩放因子
int64_t x_int = static_cast<int64_t>(x * 1e10);  // 保留10位小数

6.10 本章小结

本章我们学习了Clipper2的高级应用技巧:

  1. Z轴支持:启用USINGZ、Z值用途、Z值回调
  2. 输出格式控制:PolyTree与Paths选择、保留共线点、反转输出
  3. 错误处理:输入验证、结果验证、异常处理
  4. 性能优化:减少顶点、边界框预筛选、批量操作、并行处理、内存优化
  5. 与其他库集成:OpenGL、SVG、GeoJSON、GDAL/OGR
  6. 调试与可视化:SVG调试、打印信息、性能分析
  7. 最佳实践:代码组织、错误处理策略、配置管理
  8. 常见陷阱:坐标溢出、填充规则不匹配、精度损失

通过本章的学习,您应该能够在实际项目中更加高效、可靠地使用Clipper2库。