You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
748 lines
38 KiB
748 lines
38 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Runtime.InteropServices; |
|
using ESRI.ArcGIS.Geometry; |
|
using GeoAPI.Operation.Buffer; |
|
using KGIS.Framework.AE; |
|
using NetTopologySuite.IO; |
|
using NetTopologySuite.Operation.Buffer; |
|
|
|
namespace Kingo.PluginServiceInterface.Helper |
|
{ |
|
/// <summary> |
|
/// 狭长图形检测工具类(静态类) |
|
/// 功能:检测多边形中是否存在狭长区域(如长条状、细带状图斑) |
|
/// 核心算法思路:通过分析多边形环的顶点分布,识别局部密集点对,构建子多边形并通过紧凑度指标判断是否为狭长区域 |
|
/// </summary> |
|
public static class PolygonNarrowAreaChecker |
|
{ |
|
/// <summary> |
|
/// 检测多边形中的局部狭长区域 |
|
/// </summary> |
|
/// <param name="polygon">待检测的输入多边形(支持带内环的复杂多边形)</param> |
|
/// <param name="distanceThreshold">距离阈值(单位:与坐标系一致,默认1):用于判断两个顶点是否"过近",作为狭长区域的初步筛选条件</param> |
|
/// <returns>检测到的狭长区域多边形列表(每个元素为一个狭长子多边形)</returns> |
|
public static List<IPolygon> CheckLocalNarrowAreas(IPolygon polygon, double distanceThreshold = 1) |
|
{ |
|
List<IPolygon> narrowAreas = new List<IPolygon>(); // 存储检测到的狭长区域 |
|
//var xxx = GeometryConvertHelper.ConvertIGeoemtryToWKT(polygon); |
|
string sNarrow = ISNarrow(polygon); |
|
if (!string.IsNullOrEmpty(sNarrow)) |
|
{ |
|
#region 删除图形中的重复点(首位点除外) |
|
IGeometryCollection geomColl = polygon as IGeometryCollection; |
|
// 遍历所有子环(外环和内环) |
|
for (int i = 0; i < geomColl.GeometryCount; i++) |
|
{ |
|
IGeometry subGeom = geomColl.get_Geometry(i); |
|
IRing ring = subGeom as IRing; // 环是闭合的线(IRing 继承自 IPolyline) |
|
if (ring == null) continue; |
|
// 处理当前环的重复点 |
|
RemoveDuplicatePointsInRing(ring); |
|
} |
|
#endregion |
|
IGeometryCollection geometryCollection = polygon as IGeometryCollection;// 将多边形转换为几何集合(用于遍历内部环) |
|
if (geometryCollection == null) return narrowAreas; // 非几何集合类型直接返回空列表 |
|
for (int ringIndex = 0; ringIndex < geometryCollection.GeometryCount; ringIndex++)// 遍历多边形的所有环(外环+内环) |
|
{ |
|
IRing ring = geometryCollection.get_Geometry(ringIndex) as IRing; // 获取当前环 |
|
if (ring == null) continue; // 非环类型跳过 |
|
IPointCollection points = ring as IPointCollection;// 将环转换为点集合(用于获取顶点坐标) |
|
int pointCount = points.PointCount; // 环的总顶点数 |
|
bool isClosed = ring.IsClosed; // 判断环是否闭合(多边形环通常需闭合) |
|
int effectivePointCount = isClosed ? pointCount - 1 : pointCount; // 有效顶点数(闭合环需排除最后一个重复点) |
|
// 【步骤1】预处理环的顶点坐标:转换为结构体数组(提升计算性能)和IPoint对象数组(用于几何构建) |
|
var (ringPoints, ringPointsIPoint) = PreprocessRingPoints(points, effectivePointCount); if (ringPoints == null) continue; // 预处理失败时跳过当前环 |
|
HashSet<(double X, double Y)> usedPoints = new HashSet<(double X, double Y)>(); |
|
// 预计算所有线段的AABB(索引k对应线段k→k+1) |
|
var segmentAABBs = new (double MinX, double MaxX, double MinY, double MaxY)[effectivePointCount - 1]; |
|
for (int idx = 0; idx < effectivePointCount - 1; idx++) |
|
{ |
|
IPoint pStart = ringPointsIPoint[idx]; |
|
IPoint pEnd = ringPointsIPoint[idx + 1]; |
|
segmentAABBs[idx] = ( |
|
MinX: Math.Min(pStart.X, pEnd.X), |
|
MaxX: Math.Max(pStart.X, pEnd.X), |
|
MinY: Math.Min(pStart.Y, pEnd.Y), |
|
MaxY: Math.Max(pStart.Y, pEnd.Y) |
|
); |
|
} |
|
// 遍历所有可能的线段对(需优化遍历范围,避免O(n²)复杂度) |
|
for (int k = 0; k < effectivePointCount - 1; k++) |
|
{ |
|
IPoint p1 = ringPointsIPoint[k]; |
|
IPoint p2 = ringPointsIPoint[k + 1]; |
|
// 检查p1/p2是否已被使用(通过坐标判断) |
|
if (usedPoints.Contains((p1.X, p1.Y)) || usedPoints.Contains((p2.X, p2.Y))) continue; |
|
// 获取当前线段k的AABB |
|
var aabbK = segmentAABBs[k]; |
|
for (int m = k + 2; m < effectivePointCount - 1; m++) // 跳过相邻线段 |
|
{ |
|
IPoint p3 = ringPointsIPoint[m]; |
|
IPoint p4 = ringPointsIPoint[m + 1]; |
|
// 检查p3/p4是否已被使用 |
|
if (usedPoints.Contains((p3.X, p3.Y)) || usedPoints.Contains((p4.X, p4.Y))) continue; |
|
// 获取对比线段m的AABB |
|
var aabbM = segmentAABBs[m]; |
|
// AABB预筛选:判断两线段的包围盒是否可能相交 |
|
// 条件:x轴投影重叠且y轴投影重叠 |
|
double expandDistance = 1;//相当于外扩 |
|
bool xOverlap = (aabbK.MaxX + expandDistance) >= (aabbM.MinX - expandDistance) && |
|
(aabbK.MinX - expandDistance) <= (aabbM.MaxX + expandDistance); |
|
bool yOverlap = (aabbK.MaxY + expandDistance) >= (aabbM.MinY - expandDistance) && |
|
(aabbK.MinY - expandDistance) <= (aabbM.MaxY + expandDistance); |
|
if (!xOverlap || !yOverlap) continue; // AABB不重叠,直接跳过后续复杂计算 |
|
// 判断线段是否共点(如p1=p3, p2=p4等) |
|
if (ArePointsEqual(p1, p3) || ArePointsEqual(p1, p4) || ArePointsEqual(p2, p3) || ArePointsEqual(p2, p4)) continue; |
|
// 计算线段(p1,p2)和(p3,p4)的距离(仅保留AABB重叠的情况) |
|
Vector2D vecAB = CalculateVector(p1, p2); |
|
Vector2D vecCD = CalculateVector(p3, p4); |
|
bool isSameDirection = AreVectorsSameDirection(vecAB, vecCD, 0.01); |
|
if (isSameDirection) continue; |
|
var vangle = CalculateAngle(vecAB, vecCD); |
|
if (vangle < 25 || vangle > 155) continue; |
|
double distance = CalculateSegmentDistance(p1, p2, p3, p4); |
|
// var xxx1 = $@" |
|
//{GeometryConvertHelper.ConvertIGeoemtryToWKT(p1)} |
|
//{GeometryConvertHelper.ConvertIGeoemtryToWKT(p2)} |
|
//{GeometryConvertHelper.ConvertIGeoemtryToWKT(p3)} |
|
//{GeometryConvertHelper.ConvertIGeoemtryToWKT(p4)}"; |
|
if (distance < 0.1) |
|
{ |
|
IPoint iIntersection = GetExtendedLineIntersection(p1, p2, p3, p4); //两条线段的交点 |
|
if (iIntersection != null) |
|
{ |
|
double p1distance = PointToSegmentDistance(p1, p3, p4); |
|
double p2distance = PointToSegmentDistance(p2, p3, p4); |
|
double p3distance = PointToSegmentDistance(p3, p1, p2); |
|
double p4distance = PointToSegmentDistance(p4, p1, p2); |
|
int pindex_k = p1distance > p2distance ? k + 1 : k; |
|
int pindex_m = p3distance > p4distance ? m + 1 : m; |
|
double iIntersection12 = PointToSegmentDistance(iIntersection, p1, p2); |
|
double iIntersection34 = PointToSegmentDistance(iIntersection, p3, p4); |
|
IPoint point = null; |
|
if (iIntersection34 < iIntersection12) |
|
{ |
|
point = p1distance > p2distance ? p2 : p1; |
|
} |
|
else |
|
{ |
|
point = p3distance > p4distance ? p4 : p3; |
|
} |
|
try |
|
{ |
|
IPolyline polyline1 = new PolylineClass(); |
|
polyline1.FromPoint = iIntersection; |
|
polyline1.ToPoint = point; |
|
polyline1.SpatialReference = polygon.SpatialReference; |
|
if (polyline1.Length < 0.1) |
|
{ |
|
polyline1 = getExtendLine(polyline1, 3, 0.01); |
|
} |
|
var CutGeometryresult = FeatureAPI.CutGeometry(polygon, polyline1); |
|
Marshal.ReleaseComObject(polyline1); |
|
if (CutGeometryresult != null && CutGeometryresult.Count == 2) |
|
{ |
|
foreach (var item in CutGeometryresult) |
|
{ |
|
sNarrow = ISNarrow(item); |
|
if (sNarrow == "POLYGON EMPTY") |
|
{ |
|
List<IPoint> pointList = null; |
|
var angle = SharpAngle.GetMinAngle1(item, ref pointList, false); |
|
if (angle < 10) continue; |
|
if (isThreeSides(item)) continue; |
|
narrowAreas.Add(item as IPolygon); |
|
usedPoints.Add((p1.X, p1.Y)); |
|
usedPoints.Add((p2.X, p2.Y)); |
|
usedPoints.Add((p3.X, p3.Y)); |
|
usedPoints.Add((p4.X, p4.Y)); |
|
} |
|
else if (!string.IsNullOrEmpty(sNarrow)) |
|
{ |
|
narrowAreas = CheckLocalNarrowAreas(item as IPolygon); |
|
} |
|
} |
|
} |
|
else if (CutGeometryresult != null && CutGeometryresult.Count == 1) |
|
{ |
|
continue; |
|
//IGeometry geo = CutGeometryresult[0]; |
|
//if (geo != null && !geo.IsEmpty) |
|
//{ |
|
// sNarrow = ISNarrow(geo); |
|
// if (!string.IsNullOrEmpty(sNarrow)) |
|
// { |
|
// List<IPoint> pointList = null; |
|
// var angle = SharpAngle.GetMinAngle1(geo, ref pointList, false); |
|
// if (angle < 10) continue; |
|
// narrowAreas.Add(geo as IPolygon); |
|
// usedPoints.Add((p1.X, p1.Y)); |
|
// usedPoints.Add((p2.X, p2.Y)); |
|
// usedPoints.Add((p3.X, p3.Y)); |
|
// usedPoints.Add((p4.X, p4.Y)); |
|
// continue; |
|
// } |
|
//} |
|
//else continue; |
|
} |
|
} |
|
catch (Exception ex) |
|
{ |
|
continue; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return narrowAreas; |
|
} |
|
return new List<IPolygon>(); |
|
} |
|
|
|
private static IPoint GetExtendedLineIntersection(IPoint start1, IPoint end1, IPoint start2, IPoint end2) |
|
{ |
|
// 创建原始线段 |
|
IPolyline polyline1 = new PolylineClass(); |
|
polyline1.FromPoint = start1; |
|
polyline1.ToPoint = end1; |
|
// 延长线段(假设 getExtendLine 可能返回包含中间点的多段线) |
|
IPolyline extendedPolyline1 = getExtendLine(polyline1, 3, 1); |
|
// 提取延长后的起点和终点,重建为仅含两点的线段 |
|
IPolyline simplifiedLine1 = new PolylineClass(); |
|
simplifiedLine1.FromPoint = extendedPolyline1.FromPoint; // 延长后的起点 |
|
simplifiedLine1.ToPoint = extendedPolyline1.ToPoint; // 延长后的终点 |
|
// 对第二条线段执行相同操作 |
|
IPolyline polyline2 = new PolylineClass(); |
|
polyline2.FromPoint = start2; |
|
polyline2.ToPoint = end2; |
|
IPolyline extendedPolyline2 = getExtendLine(polyline2, 3, 1); |
|
IPolyline simplifiedLine2 = new PolylineClass(); |
|
simplifiedLine2.FromPoint = extendedPolyline2.FromPoint; |
|
simplifiedLine2.ToPoint = extendedPolyline2.ToPoint; |
|
// 计算简化后线段的交点 |
|
ITopologicalOperator topoOp = simplifiedLine1 as ITopologicalOperator; |
|
IGeometry intersection = topoOp.Intersect(simplifiedLine2, esriGeometryDimension.esriGeometry0Dimension); |
|
// 处理交点结果(与原逻辑一致) |
|
if (intersection != null && intersection.GeometryType == esriGeometryType.esriGeometryPoint) |
|
{ |
|
return intersection as IPoint; |
|
} |
|
else if (intersection.GeometryType == esriGeometryType.esriGeometryMultipoint) |
|
{ |
|
IPointCollection points = intersection as IPointCollection; |
|
if (points.PointCount > 0) |
|
return points.get_Point(0); |
|
} |
|
return null; |
|
} |
|
/// <summary> |
|
/// 延长线段 |
|
/// </summary> |
|
/// <param name="passLine">传入去的线</param> |
|
/// <param name="mode">模式,1为从FromPoint处延长,2为从ToPint处延长,3为两端延长</param> |
|
/// <param name="dis">延长的距离</param> |
|
/// <returns></returns> |
|
/// 创建人:懒羊羊 |
|
public static IPolyline getExtendLine(IPolyline passLine, int mode, double dis) |
|
{ |
|
IPointCollection pPointCol = passLine as IPointCollection; |
|
switch (mode) |
|
{ |
|
case 1: |
|
IPoint fromPoint = new PointClass(); |
|
passLine.QueryPoint(esriSegmentExtension.esriExtendAtFrom, -1 * dis, false, fromPoint); |
|
pPointCol.InsertPoints(0, 1, ref fromPoint); |
|
break; |
|
case 2: |
|
IPoint endPoint = new PointClass(); |
|
object missing = Type.Missing; |
|
passLine.QueryPoint(esriSegmentExtension.esriExtendAtTo, dis + passLine.Length, false, endPoint); |
|
pPointCol.AddPoint(endPoint, ref missing, ref missing); |
|
break; |
|
case 3: |
|
IPoint fPoint = new PointClass(); |
|
IPoint ePoint = new PointClass(); |
|
object missing2 = Type.Missing; |
|
passLine.QueryPoint(esriSegmentExtension.esriExtendAtFrom, -1 * dis, false, fPoint); |
|
pPointCol.InsertPoints(0, 1, ref fPoint); |
|
passLine.QueryPoint(esriSegmentExtension.esriExtendAtTo, dis + passLine.Length, false, ePoint); |
|
pPointCol.AddPoint(ePoint, ref missing2, ref missing2); |
|
break; |
|
default: |
|
return pPointCol as IPolyline; |
|
} |
|
return pPointCol as IPolyline; |
|
} |
|
/// <summary> |
|
/// 判断图形是否为三角形 |
|
/// </summary> |
|
/// <param name="geometry"></param> |
|
/// <returns></returns> |
|
private static bool isThreeSides(IGeometry geometry) |
|
{ |
|
bool isThreeSides = false; |
|
try |
|
{ |
|
if (geometry is IPolygon polygon1) |
|
{ |
|
IPointCollection pointCollection = polygon1 as IPointCollection; |
|
int vertexCount = pointCollection.PointCount; |
|
// 排除闭合多边形中重复的首尾顶点 |
|
IPoint startingpoint = pointCollection.get_Point(0);//起点 |
|
IPoint terminalpoint = pointCollection.get_Point(vertexCount - 1);//终点 |
|
if (polygon1.IsClosed && vertexCount > 1 && startingpoint.X == terminalpoint.X && startingpoint.Y == terminalpoint.Y) |
|
{ |
|
vertexCount--; |
|
} |
|
isThreeSides = vertexCount == 3; |
|
} |
|
} |
|
catch (Exception ex) |
|
{ |
|
|
|
} |
|
return isThreeSides; |
|
} |
|
/// <summary> |
|
/// 判断两个点是否共点(考虑浮点精度误差) |
|
/// </summary> |
|
/// <param name="p1">点1</param> |
|
/// <param name="p2">点2</param> |
|
/// <param name="tolerance">容差(默认1e-6,与坐标系单位一致)</param> |
|
/// <returns>true:共点;false:不共点</returns> |
|
private static bool ArePointsEqual(IPoint p1, IPoint p2, double tolerance = 1e-6) |
|
{ |
|
return Math.Abs(p1.X - p2.X) < tolerance && Math.Abs(p1.Y - p2.Y) < tolerance; |
|
} |
|
/// <summary> |
|
/// 计算两条线段的最短距离(支持不相交、相交等场景) |
|
/// </summary> |
|
/// <param name="a1">线段1的起点</param> |
|
/// <param name="a2">线段1的终点</param> |
|
/// <param name="b1">线段2的起点</param> |
|
/// <param name="b2">线段2的终点</param> |
|
/// <returns>两条线段的最短距离</returns> |
|
private static double CalculateSegmentDistance(IPoint a1, IPoint a2, IPoint b1, IPoint b2) |
|
{ |
|
// 向量定义:u为a线段方向,v为b线段方向,w为a1到b1的向量 |
|
double ux = a2.X - a1.X; |
|
double uy = a2.Y - a1.Y; |
|
double vx = b2.X - b1.X; |
|
double vy = b2.Y - b1.Y; |
|
double wx = a1.X - b1.X; |
|
double wy = a1.Y - b1.Y; |
|
double a = ux * ux + uy * uy; // u·u(a线段长度平方) |
|
double b = ux * vx + uy * vy; // u·v(方向向量点积) |
|
double c = vx * vx + vy * vy; // v·v(b线段长度平方) |
|
double d = ux * wx + uy * wy; // u·w |
|
double e = vx * wx + vy * wy; // v·w |
|
double denominator = a * c - b * b; // 分母:(u·u)(v·v)-(u·v)² |
|
double t, s; |
|
// 情况1:线段平行或共线(分母为0) |
|
if (Math.Abs(denominator) < 1e-9) |
|
{ |
|
t = 0; // a线段起点投影到b线段 |
|
s = (b * t + e) / c; // 计算s(若c=0则b线段为点) |
|
} |
|
// 情况2:线段不平行,计算参数t和s |
|
else |
|
{ |
|
t = (b * e - c * d) / denominator; // 投影参数t(a线段上) |
|
s = (a * e - b * d) / denominator; // 投影参数s(b线段上) |
|
} |
|
// 限制t和s在[0,1]范围内(超出则取端点) |
|
t = Math.Max(0, Math.Min(1, t)); |
|
s = Math.Max(0, Math.Min(1, s)); |
|
// 计算投影点距离 |
|
double dx = (wx + ux * t - vx * s); |
|
double dy = (wy + uy * t - vy * s); |
|
return Math.Sqrt(dx * dx + dy * dy); |
|
} |
|
private static double PointToSegmentDistance(IPoint p, IPoint segStart, IPoint segEnd) |
|
{ |
|
double segX = segEnd.X - segStart.X; |
|
double segY = segEnd.Y - segStart.Y; |
|
if (segX == 0 && segY == 0) // 线段为点 |
|
return Math.Sqrt(Math.Pow(p.X - segStart.X, 2) + Math.Pow(p.Y - segStart.Y, 2)); |
|
double t = ((p.X - segStart.X) * segX + (p.Y - segStart.Y) * segY) / (segX * segX + segY * segY); |
|
t = Math.Max(0, Math.Min(1, t)); // 投影参数限制在[0,1] |
|
double projX = segStart.X + t * segX; |
|
double projY = segStart.Y + t * segY; |
|
return Math.Sqrt(Math.Pow(p.X - projX, 2) + Math.Pow(p.Y - projY, 2)); |
|
} |
|
/// <summary> |
|
/// 预处理环的顶点坐标(结构体数组和IPoint数组) |
|
/// </summary> |
|
/// <param name="points">环的顶点集合(IPoint类型)</param> |
|
/// <param name="effectivePointCount">有效顶点数(闭合环需排除最后一个重复点)</param> |
|
/// <returns>元组:(顶点坐标结构体数组, IPoint对象数组);若异常则返回(null, null)</returns> |
|
private static ((double X, double Y)[], IPoint[]) PreprocessRingPoints(IPointCollection points, int effectivePointCount) |
|
{ |
|
try |
|
{ |
|
var ringPoints = new (double X, double Y)[effectivePointCount]; // 存储顶点坐标(结构体,轻量级) |
|
var ringPointsIPoint = new IPoint[effectivePointCount]; // 存储IPoint对象(用于几何操作) |
|
// 遍历有效顶点,填充两个数组 |
|
for (int k = 0; k < effectivePointCount; k++) |
|
{ |
|
IPoint p = points.get_Point(k); // 获取第k个顶点 |
|
ringPoints[k] = (p.X, p.Y); // 转换为结构体 |
|
ringPointsIPoint[k] = p; // 保留IPoint对象 |
|
} |
|
return (ringPoints, ringPointsIPoint); |
|
} |
|
catch (Exception ex) |
|
{ |
|
// 异常处理:顶点读取失败时记录日志(实际开发中建议添加日志记录) |
|
return (null, null); |
|
} |
|
} |
|
/// <summary> |
|
/// 判断两个向量是否同向 |
|
/// </summary> |
|
/// <param name="vec1">向量1(如AB)</param> |
|
/// <param name="vec2">向量2(如CD)</param> |
|
/// <param name="tolerance">浮点数精度容差(默认1e-6)</param> |
|
/// <returns>是否同向</returns> |
|
public static bool AreVectorsSameDirection(Vector2D vec1, Vector2D vec2, double tolerance = 1e-6) |
|
{ |
|
// 1. 判断共线性:叉积的绝对值小于容差(考虑浮点数精度) |
|
double crossProduct = vec1.X * vec2.Y - vec2.X * vec1.Y; |
|
if (Math.Abs(crossProduct) > tolerance) |
|
return false; // 不共线,直接返回false |
|
// 2. 判断同向性:点积大于0(且向量模长不为0,避免零向量干扰) |
|
double dotProduct = vec1.X * vec2.X + vec1.Y * vec2.Y; |
|
double length1 = CalculateVectorLength(vec1); |
|
double length2 = CalculateVectorLength(vec2); |
|
// 模长为0的向量无方向(实际线段向量不会为零,此处为安全校验) |
|
if (length1 < tolerance || length2 < tolerance) |
|
return false; |
|
return dotProduct > tolerance; // 点积为正,方向相同 |
|
} |
|
/// <summary> |
|
/// 计算向量模长 |
|
/// </summary> |
|
public static double CalculateVectorLength(Vector2D vector) |
|
{ |
|
return Math.Sqrt(Math.Pow(vector.X, 2) + Math.Pow(vector.Y, 2)); |
|
} |
|
/// <summary> |
|
/// 计算线段向量 |
|
/// </summary> |
|
/// <param name="startPoint">线段起点</param> |
|
/// <param name="endPoint">线段终点</param> |
|
/// <returns>向量</returns> |
|
public static Vector2D CalculateVector(IPoint startPoint, IPoint endPoint) |
|
{ |
|
if (startPoint == null || endPoint == null) |
|
throw new ArgumentNullException("点对象不能为空"); |
|
|
|
return new Vector2D( |
|
endPoint.X - startPoint.X, |
|
endPoint.Y - startPoint.Y |
|
); |
|
} |
|
/// <summary> |
|
/// 计算两个向量的夹角(角度制,0°~180°) |
|
/// </summary> |
|
/// <param name="vec1">向量1</param> |
|
/// <param name="vec2">向量2</param> |
|
/// <param name="tolerance">精度容差(默认1e-6)</param> |
|
/// <returns>夹角角度(°)</returns> |
|
public static double CalculateAngle(Vector2D vec1, Vector2D vec2, double tolerance = 1e-6) |
|
{ |
|
// 计算模长 |
|
double len1 = CalculateVectorLength(vec1); |
|
double len2 = CalculateVectorLength(vec2); |
|
// 处理零向量(避免除零异常) |
|
if (len1 < tolerance || len2 < tolerance) |
|
throw new ArgumentException("向量模长不能为零"); |
|
// 计算点积 |
|
double dotProduct = vec1.X * vec2.X + vec1.Y * vec2.Y; |
|
// 计算cosθ(点积/(模长乘积)) |
|
double cosTheta = dotProduct / (len1 * len2); |
|
// 修正数值精度误差(cosθ范围需在[-1, 1]内) |
|
cosTheta = Math.Max(Math.Min(cosTheta, 1.0), -1.0); |
|
// 计算弧度并转换为角度 |
|
double radians = Math.Acos(cosTheta); |
|
return radians * (180 / Math.PI); // 弧度转角度 |
|
} |
|
|
|
// 定义向量结构体 |
|
public struct Vector2D |
|
{ |
|
public double X; |
|
public double Y; |
|
|
|
public Vector2D(double x, double y) |
|
{ |
|
X = x; |
|
Y = y; |
|
} |
|
} |
|
|
|
#region 节点去重 |
|
private static void RemoveDuplicatePointsInRing(IRing ring) |
|
{ |
|
IPointCollection pointColl = ring as IPointCollection; |
|
if (pointColl == null || pointColl.PointCount <= 1) return; |
|
// 步骤1:提取所有点到临时列表 |
|
List<IPoint> allPoints = new List<IPoint>(); |
|
for (int i = 0; i < pointColl.PointCount; i++) |
|
{ |
|
allPoints.Add(pointColl.get_Point(i)); |
|
} |
|
// 步骤2:全局去重(支持X/Y/Z/M全维度比较) |
|
HashSet<string> seenCoordinates = new HashSet<string>(); |
|
List<IPoint> uniquePoints = new List<IPoint>(); |
|
foreach (IPoint point in allPoints) |
|
{ |
|
// 生成坐标唯一键(根据需求调整是否包含Z/M) |
|
string coordKey = GetCoordinateKey(point); |
|
if (!seenCoordinates.Contains(coordKey)) |
|
{ |
|
seenCoordinates.Add(coordKey); |
|
uniquePoints.Add(point); |
|
} |
|
} |
|
// 步骤3:重建点集合(先清空再添加去重点) |
|
pointColl.RemovePoints(0, pointColl.PointCount); // 清空原始点 |
|
foreach (IPoint uniquePoint in uniquePoints) |
|
{ |
|
pointColl.AddPoint(uniquePoint); |
|
} |
|
// 步骤4:确保环闭合(最后一点与第一点一致) |
|
ring.Close(); |
|
} |
|
|
|
// 生成坐标唯一键(可根据需求调整比较维度) |
|
private static string GetCoordinateKey(IPoint point) |
|
{ |
|
// 仅比较X/Y(忽略Z/M) |
|
double x = point.X; |
|
double y = point.Y; |
|
return $"{x:F6},{y:F6}"; // 保留6位小数避免精度问题 |
|
} |
|
#endregion |
|
|
|
/// <summary> |
|
/// 判断图形是否存在狭长 |
|
/// </summary> |
|
/// <param name="geometry"></param> |
|
/// <returns></returns> |
|
private static string ISNarrow(IGeometry geometry) |
|
{ |
|
try |
|
{ |
|
var strgeometry = GeometryConvertHelper.ConvertIGeoemtryToWKT(geometry); |
|
NetTopologySuite.Geometries.Geometry ntsGeometry = (NetTopologySuite.Geometries.Geometry)new WKBReader().Read(GeometryConvertHelper.ConvertGeometryToWKB(geometry)); |
|
// 内缩参数(侵蚀) |
|
var innerBufferParams = new BufferParameters |
|
{ |
|
QuadrantSegments = 30, // 内缩平滑度 |
|
JoinStyle = JoinStyle.Bevel, // 斜角连接避免尖锐残留 |
|
EndCapStyle = EndCapStyle.Square // 平端帽处理开放线段 |
|
}; |
|
var ntsGeometry1 = ntsGeometry.Buffer(-0.05, innerBufferParams); // 内缩0.1 |
|
strgeometry = ntsGeometry1.AsText(); |
|
if (strgeometry == "POLYGON EMPTY") |
|
{ |
|
return strgeometry; |
|
} |
|
// 外扩参数(膨胀) |
|
var outerBufferParams = new BufferParameters |
|
{ |
|
QuadrantSegments = 200, // 高平滑度外扩 |
|
JoinStyle = JoinStyle.Mitre, |
|
MitreLimit = 2 // 限制尖锐拐角延伸长度 |
|
}; |
|
var ntsGeometry2 = ntsGeometry1.Buffer(0.05, outerBufferParams); // 外扩0.1 |
|
strgeometry = ntsGeometry2.AsText(); |
|
double length1 = ntsGeometry.Length; |
|
int pointCount1 = ntsGeometry.NumPoints; |
|
double length2 = ntsGeometry2.Length; |
|
int pointCount2 = ntsGeometry2.NumPoints; |
|
double delta_length = length1 - length2; |
|
double avg_halfangle = 180 * (pointCount1 - 1 - 2) / (pointCount1 - 1) / 2; |
|
double conner_normal_length = 2 * 0.05 / Math.Tan(avg_halfangle * (Math.PI / 180)); |
|
if (delta_length > 8 * conner_normal_length * (pointCount1 - 1)) |
|
{ |
|
return strgeometry; |
|
} |
|
} |
|
catch (Exception ex) |
|
{ |
|
} |
|
return string.Empty; |
|
} |
|
|
|
} |
|
|
|
public class EarClipper |
|
{ |
|
// 三角剖分结果(每个三角形为3个点的集合) |
|
public List<List<IPoint>> Triangles { get; private set; } |
|
|
|
public EarClipper() |
|
{ |
|
Triangles = new List<List<IPoint>>(); |
|
} |
|
// 多边形整体方向(顺时针/逆时针) |
|
private bool _isCounterClockwise; |
|
/// <summary> |
|
/// 对多边形执行耳切法三角剖分 |
|
/// </summary> |
|
/// <param name="polygon">输入多边形(需为简单多边形,无自交)</param> |
|
/// <returns>三角剖分结果</returns> |
|
public bool Clip(IPolygon polygon) |
|
{ |
|
if (polygon == null || polygon.IsEmpty) |
|
return false; |
|
// 提取多边形外环的顶点(简单多边形默认处理外环) |
|
IPointCollection pointCollection = polygon as IPointCollection; |
|
if (pointCollection == null || pointCollection.PointCount < 3) |
|
return false; |
|
// 转换为顶点列表(排除最后一个闭合点) |
|
List<IPoint> vertices = new List<IPoint>(); |
|
for (int i = 0; i < pointCollection.PointCount - 1; i++) |
|
{ |
|
IPoint p = pointCollection.get_Point(i); |
|
vertices.Add(p); |
|
} |
|
// 关键:计算多边形整体方向(顺时针/逆时针) |
|
_isCounterClockwise = IsPolygonCounterClockwise(vertices); |
|
// 执行耳切法 |
|
Triangles.Clear(); |
|
ClipEars(vertices); |
|
return Triangles.Count > 0; |
|
} |
|
|
|
/// <summary> |
|
/// 迭代切割耳 |
|
/// </summary> |
|
private void ClipEars(List<IPoint> vertices) |
|
{ |
|
if (vertices.Count < 3) return; |
|
int index = 0; |
|
while (vertices.Count > 3) |
|
{ |
|
// 循环取顶点(处理索引越界) |
|
int prevIndex = (index - 1 + vertices.Count) % vertices.Count; |
|
int currIndex = index % vertices.Count; |
|
int nextIndex = (index + 1) % vertices.Count; |
|
IPoint prev = vertices[prevIndex]; |
|
IPoint curr = vertices[currIndex]; |
|
IPoint next = vertices[nextIndex]; |
|
// 检查当前三点是否构成"耳" |
|
if (IsConvex(prev, curr, next) && IsEar(prev, curr, next, vertices)) |
|
{ |
|
// 记录三角形 |
|
Triangles.Add(new List<IPoint> { prev, curr, next }); |
|
// 移除当前顶点(切割耳) |
|
vertices.RemoveAt(currIndex); |
|
// 重置索引(从下一个顶点开始检查) |
|
index = 0; |
|
} |
|
else |
|
{ |
|
index++; |
|
// 防止死循环(理论上简单多边形必然有耳) |
|
if (index >= vertices.Count * 2) |
|
break; |
|
} |
|
} |
|
// 添加最后一个三角形 |
|
if (vertices.Count == 3) |
|
{ |
|
Triangles.Add(new List<IPoint> { vertices[0], vertices[1], vertices[2] }); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// 判断顶点curr是否为凸点(通过叉积) |
|
/// </summary> |
|
private bool IsConvex(IPoint prev, IPoint curr, IPoint next) |
|
{ |
|
// 计算向量:prev->curr 和 curr->next |
|
double v1x = curr.X - prev.X; |
|
double v1y = curr.Y - prev.Y; |
|
double v2x = next.X - curr.X; |
|
double v2y = next.Y - curr.Y; |
|
// 叉积(z分量):v1 × v2 = v1x*v2y - v1y*v2x |
|
double cross = v1x * v2y - v1y * v2x; |
|
// 叉积≥0:逆时针多边形的凸点(根据多边形方向调整符号) |
|
// 关键:根据多边形整体方向判断凸点 |
|
if (_isCounterClockwise) |
|
{ |
|
// 逆时针多边形:叉积 >= 0 为凸点 |
|
return cross >= 0; |
|
} |
|
else |
|
{ |
|
// 顺时针多边形:叉积 <= 0 为凸点(与逆时针相反) |
|
return cross <= 0; |
|
} |
|
} |
|
/// <summary> |
|
/// 判断多边形顶点是顺时针还是逆时针排列 |
|
/// </summary> |
|
private bool IsPolygonCounterClockwise(List<IPoint> vertices) |
|
{ |
|
double sum = 0; |
|
int n = vertices.Count; |
|
for (int i = 0; i < n; i++) |
|
{ |
|
IPoint p = vertices[i]; |
|
IPoint q = vertices[(i + 1) % n]; // 下一个顶点(首尾相连) |
|
sum += (q.X - p.X) * (q.Y + p.Y); |
|
} |
|
// sum > 0 表示逆时针,sum < 0 表示顺时针 |
|
return sum > 0; |
|
} |
|
|
|
/// <summary> |
|
/// 判断三点组成的三角形是否为"耳"(内部无其他顶点) |
|
/// </summary> |
|
private bool IsEar(IPoint a, IPoint b, IPoint c, List<IPoint> vertices) |
|
{ |
|
// 检查所有其他顶点是否在三角形abc内部 |
|
foreach (IPoint p in vertices) |
|
{ |
|
// 跳过a、b、c本身 |
|
if (p.Equals(a) || p.Equals(b) || p.Equals(c)) |
|
continue; |
|
// 判断点p是否在三角形abc内部 |
|
if (IsPointInTriangle(p, a, b, c)) |
|
return false; |
|
} |
|
return true; |
|
} |
|
|
|
/// <summary> |
|
/// 判断点p是否在三角形abc内部( barycentric坐标法) |
|
/// </summary> |
|
private bool IsPointInTriangle(IPoint p, IPoint a, IPoint b, IPoint c) |
|
{ |
|
double v0x = c.X - a.X; |
|
double v0y = c.Y - a.Y; |
|
double v1x = b.X - a.X; |
|
double v1y = b.Y - a.Y; |
|
double v2x = p.X - a.X; |
|
double v2y = p.Y - a.Y; |
|
// 计算点积 |
|
double dot00 = v0x * v0x + v0y * v0y; |
|
double dot01 = v0x * v1x + v0y * v1y; |
|
double dot02 = v0x * v2x + v0y * v2y; |
|
double dot11 = v1x * v1x + v1y * v1y; |
|
double dot12 = v1x * v2x + v1y * v2y; |
|
// 计算 barycentric坐标 |
|
double invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01); |
|
double u = (dot11 * dot02 - dot01 * dot12) * invDenom; |
|
double v = (dot00 * dot12 - dot01 * dot02) * invDenom; |
|
// 点在三角形内(包括边界) |
|
return (u >= 0) && (v >= 0) && (u + v <= 1); |
|
} |
|
} |
|
} |