blob: 78fcdc0a149e654bd8f7a57c24b196f257696fc2 [file] [log] [blame]
/*
*
* Copyright (c) 2025 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Utility Class providing functionality around various checks required for
* zones, such as self-intersection, duplicates, etc.
*
*/
#pragma once
#include <app-common/zap-generated/cluster-objects.h>
#include <set>
#include <vector>
namespace chip {
namespace app {
namespace Clusters {
namespace ZoneManagement {
enum class OrientationEnum : uint8_t
{
kCollinear = 0x00,
kOnLeft = 0x01,
kOnRight = 0x02,
};
class ZoneGeometry
{
public:
using TwoDCartesianVertexStruct = Structs::TwoDCartesianVertexStruct::Type;
static bool AreTwoDCartVerticesEqual(const TwoDCartesianVertexStruct & v1, const TwoDCartesianVertexStruct & v2)
{
return v1.x == v2.x && v1.y == v2.y;
}
/**
* The Spec requires zones to be a simple polygon. That is a piecewise-linear
* continuous map from a circle to the plane (a loop) that is not
* self-intersecting (i.e. is a 1-1 or injective map).
*
* Being a map from a circle, continuity, and piecewise-linearity are guaranteed
* by the way we define the zone in terms of an ordered set of vertices that we connect in
* a cycle, so we only need to check that the map is 1-1.
*
* Algorithm for Checking Self-Intersection of a Zone
* --------------------------------------------------
*
* 1. If any vertex appears more than once in the list of vertices, the mapping
* is not 1-1. So, the zone is self-intersecting.
* 2. For a polygon of 3 vertices, flag it as self-intersecting if all
* vertices are collinear. Otherwise, it is a simple triangle and a valid
* polygon.
* 3. For all polygons of > 3 vertices, go through all pairs of non-adjacent edges
* p1q1 and p2q2. If any non-adjacent edges intersect, the zone is
* self-intersecting. If all such pairs do not intersect, the zone is not
* self-intersecting.
*
* Algorithm for SegmentsIntersect(p1q1, p2q2)
* -------------------------------------------
* 1. If points p1 and q1 lie on the opposite sides of segment p2q2 AND points p2
* and q2 lie on opposite sides of segment p1q1, the segments intersect.
* Orientation(pq, r) is used to determine which side of the vector pq point r
* lies on, or whether it lies on the line containing pq.
* 2. If either endpoint of one segment lies on the other segment, the segments
* intersect.
* 3. In all other cases, the segments do not intersect.
*
* Algorithm for Orientation(pq, r)
* ----------------------------------
* 1. Compute cross-product of vectors pq and pr.
* 2. If cross-product > 0:
* Point r lies to the left of ray pq (Counter-clockwise from pq to pr)
* Else if cross-product < 0:
* Point r lies to the right of ray pq (Clockwise from pq to pr)
* Else
* Point r is collinear with pq
*/
/**
* Proof of Correctness of Overall Self-Intersection Algorithm
* -----------------------------------------------------------
*
* 1. As noted in the algorithm description, if a vertex is repeated the zone is
* self-intersecting. In what follows, we can, therefore, assume no repeated
* vertices and no zero-length edges(consecutive repeated vertices).
* 2. For the 3-vertex case, since the edges are all adjacent to each other, the
* only way for two edges to intersect (i.e. share a point other than the vertex
* where they are adjacent), is for them to overlap. But then all three of the
* vertices have to be collinear, so it's enough to check for that to detect
* self-intersections.
* 3. When there are 4 or more distinct vertices in the polygon, let's consider
* the case when two adjacent edges intersect. As noted in point 2, the edges
* must overlap. Since there are no repeated vertices, the two adjacent edges
* must have different lengths. Let 'l' denote the longer edge and 's'
* denote the shorter edge. The non-shared end vertex of 's', thus, lies on 'l'.
* Let this point be P. Given the polygon is a cycle with each vertex having
* degree 2, there must be two edges that meet at P. One of them is 's';
* Let's call the other one 't'. Since there are at least 4 vertices, there
* are also at least 4 edges. Because 's' and 't' are adjacent, 't' is not
* adjacent to 'l' (otherwise, there would only be 3 edges!). So, in this case,
* we have non-adjacent edges 't' and 'l' that intersect (at point P).
*
* Therefore, if there are any self-intersection in a polygon with 4 or more
* vertices, there must be intersecting non-adjacent edges.
*/
/*
* Proofs of correctness for SegmentsIntersect and Orientation
* -----------------------------------------------------------
*
* 1. Correctness of SegmentsIntersect(p1q1, p2q2) :
* a. If points p1 and q1 lie on opposite sides of p2q2, then segment p1q1
* intersects the line containing p2q2. Similarly, if points p2 and q2 lie
* on opposite sides of p1q1, then the segment p2q2 intersects the line
* containing p1q1. Since the two lines can only intersect at one point,
* this point must also lie on both segments, and the two segments intersect.
* b. If the endpoint of one of the segments lies on the other segment, then
* they intersect at that point.
* c. If the two segments intersect at all at some point P, then either P
* is the endpoint of one of the segments (and thus we are in
* the case described in (b)), or it's in the interior of both segments.
* If P is in the interior of both segments and the segments are not
* collinear, then we are in the case described in (a). If the two
* segments _are_ collinear and have a shared point P that is not an
* endpoint, then the endpoint (p1, q1, p2, or q2) closest to P has to lie
* on both segments and hence we are in the case described in (b).
*
* 2. Correctness of Orientation(p, q, r):
* a. Compute cross-product of vectors pq and pr.
* --The sign determines the direction of thumb in the right hand
* rule of turning from vector pq to pr.
* b. If cross-product > 0, r lies to the left of ray
* pq (Counter-clockwise turn from vector pq to pr)
* c. If cross-product < 0, r lies to the right of ray
* pq (Clockwise turn from vector pq to pr)
* d. If cross-product is 0, p, q, r are collinear.
*
*/
// Method to check for self-intersecting zones
static bool IsZoneSelfIntersecting(const std::vector<TwoDCartesianVertexStruct> & vertices)
{
size_t vertexCount = vertices.size();
// Check if there are duplicate vertices in the polygon
if (ZoneHasDuplicates(vertices))
{
ChipLogDetail(Zcl, "Zone has duplicate vertices");
return true;
}
// If there are only 3 vertices, it's enough to check whether they are collinear.
if (vertexCount == 3)
{
if (Orientation(vertices[0], vertices[1], vertices[2]) == OrientationEnum::kCollinear)
{
ChipLogDetail(Zcl, "Degenerate case of 3 collinear vertices");
return true;
}
return false;
}
// Iterate through all pairs of non-adjacent segments
for (size_t i = 0; i < vertexCount; ++i)
{
auto & p1 = vertices[i];
auto & q1 = vertices[(i + 1) % vertexCount];
// j starts from i+2, skipping the adjacent edge (vertices[i+1], vertices[i+2])
for (size_t j = i + 2; j < vertexCount; ++j)
{
// Skip edges that share an endpoint (i.e., adjacent edges)
if ((j + 1) % vertexCount == i)
{
continue;
}
auto & p2 = vertices[j];
auto & q2 = vertices[(j + 1) % vertexCount];
if (DoSegmentsIntersect(p1, q1, p2, q2))
{
ChipLogDetail(Zcl, "Zone segment[(%u,%u),(%u,%u)] intersects with segment[(%u,%u),(%u,%u)]", p1.x, p1.y, q1.x,
q1.y, p2.x, p2.y, q2.x, q2.y);
return true; // Found a self-intersection
}
}
}
return false; // No self-intersection found
}
private:
// Helper function: Check if point q lies on segment pr (assuming p, q, r are collinear)
// Checks if q is within the bounding box defined by p and r.
// Returns true if the coordinates of q are greater than the min and smaller
// than the max of the corresponding coordinates of p and r.
static bool OnSegment(const TwoDCartesianVertexStruct & p, const TwoDCartesianVertexStruct & q,
const TwoDCartesianVertexStruct & r)
{
return q.x <= std::max(p.x, r.x) && q.x >= std::min(p.x, r.x) && q.y <= std::max(p.y, r.y) && q.y >= std::min(p.y, r.y);
}
// Helper function: Determine the orientation of the point r with respect to ray pq: in
// the left half-plane when facing in the direction of the ray, in the right half-plane,
// or collinear with the ray.
// Employs the cross-product computation (determinant of the coordinate matrix) of
// vectors pq and pr to find the direction of the Z axis. The rotation
// considered is from vector pq to pr.
// The sign of the determinant is used to infer the direction of the cross-product vector
// and hence the orientation of the point with respect to the ray. A value of 0 indicates that the points
// are collinear.
// Using the right-hand rule Z > 0 means pointing upward from the 2D
// plane (counter-clockwise rotation) and Z < 0 means pointing downward into
// the 2D plane (clock-wise rotation).
// 0 --> p, q and r are collinear
// 1 --> Counterclockwise
// 2 --> Clockwise
static OrientationEnum Orientation(const TwoDCartesianVertexStruct & p, const TwoDCartesianVertexStruct & q,
const TwoDCartesianVertexStruct & r)
{
long long val = (static_cast<long long>(q.x - p.x) * static_cast<long long>(r.y - p.y)) -
(static_cast<long long>(q.y - p.y) * static_cast<long long>(r.x - p.x));
if (val == 0) // Collinear
{
return OrientationEnum::kCollinear;
}
return (val > 0) ? OrientationEnum::kOnLeft : OrientationEnum::kOnRight;
}
// Helper function: Check if segment p1q1 intersects segment p2q2
static bool DoSegmentsIntersect(const TwoDCartesianVertexStruct & p1, const TwoDCartesianVertexStruct & q1,
const TwoDCartesianVertexStruct & p2, const TwoDCartesianVertexStruct & q2)
{
// Segment 1 (p1q1) and first vertex of Segment 2 (p2)
OrientationEnum o1 = Orientation(p1, q1, p2);
// Segment 1(p1q1) and second vertex of Segment 2(q2)
OrientationEnum o2 = Orientation(p1, q1, q2);
// Segment 2(p2q2) and first vertex of Segment 1(p1)
OrientationEnum o3 = Orientation(p2, q2, p1);
// Segment 2(p2q2) and second vertex of Segment 1(q1)
OrientationEnum o4 = Orientation(p2, q2, q1);
// General case
if (o1 != OrientationEnum::kCollinear && o2 != OrientationEnum::kCollinear && o3 != OrientationEnum::kCollinear &&
o4 != OrientationEnum::kCollinear && o1 != o2 && o3 != o4)
{
return true;
}
// Special Cases (Collinear points)
if (o1 == OrientationEnum::kCollinear && OnSegment(p1, p2, q1))
{
return true; // p1, q1 and p2 are collinear and p2 lies on segment p1q1
}
if (o2 == OrientationEnum::kCollinear && OnSegment(p1, q2, q1))
{
return true; // p1, q1 and q2 are collinear and q2 lies on segment p1q1
}
if (o3 == OrientationEnum::kCollinear && OnSegment(p2, p1, q2))
{
return true; // p2, q2 and p1 are collinear and p1 lies on segment p2q2
}
if (o4 == OrientationEnum::kCollinear && OnSegment(p2, q1, q2))
{
return true; // p2, q2 and q1 are collinear and q1 lies on segment p2q2
}
return false;
}
struct VertexComparator
{
bool operator()(const TwoDCartesianVertexStruct & a, const TwoDCartesianVertexStruct & b) const
{
if (a.x != b.x)
{
return a.x < b.x;
}
return a.y < b.y;
}
};
static bool ZoneHasDuplicates(const std::vector<TwoDCartesianVertexStruct> & zoneVertices)
{
std::set<TwoDCartesianVertexStruct, VertexComparator> seenVertices;
for (const auto & vertex : zoneVertices)
{
if (!seenVertices.insert(vertex).second)
{
return true;
}
}
return false;
}
};
} // namespace ZoneManagement
} // namespace Clusters
} // namespace app
} // namespace chip