Skip to content

Commit abed1d0

Browse files
fix: fixed ruff errors
1 parent 9861d8a commit abed1d0

File tree

1 file changed

+68
-63
lines changed

1 file changed

+68
-63
lines changed

geometry/jarvis_march.py

Lines changed: 68 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
1616
REFERENCES:
1717
-> Wikipedia reference: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
18-
-> GeeksforGeeks: https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
18+
-> GeeksforGeeks:
19+
https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
1920
"""
2021

2122
from __future__ import annotations
@@ -49,9 +50,9 @@ def _cross_product(origin: Point, point_a: Point, point_b: Point) -> float:
4950
= 0: Collinear
5051
< 0: Clockwise turn (right turn)
5152
"""
52-
return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * (
53-
point_b.x - origin.x
54-
)
53+
return (point_a.x - origin.x) * (point_b.y - origin.y) - (
54+
point_a.y - origin.y
55+
) * (point_b.x - origin.x)
5556

5657

5758
def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool:
@@ -68,12 +69,62 @@ def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool:
6869
) <= point.y <= max(p1.y, p2.y)
6970

7071

72+
def _find_leftmost_point(points: list[Point]) -> int:
73+
"""Find index of leftmost point (and bottom-most in case of tie)."""
74+
left_idx = 0
75+
for i in range(1, len(points)):
76+
if points[i].x < points[left_idx].x or (
77+
points[i].x == points[left_idx].x and points[i].y < points[left_idx].y
78+
):
79+
left_idx = i
80+
return left_idx
81+
82+
83+
def _find_next_hull_point(points: list[Point], current_idx: int) -> int:
84+
"""Find the next point on the convex hull."""
85+
next_idx = (current_idx + 1) % len(points)
86+
# Ensure next_idx is not the same as current_idx
87+
while next_idx == current_idx:
88+
next_idx = (next_idx + 1) % len(points)
89+
90+
for i in range(len(points)):
91+
if i == current_idx:
92+
continue
93+
cross = _cross_product(points[current_idx], points[i], points[next_idx])
94+
if cross > 0:
95+
next_idx = i
96+
97+
return next_idx
98+
99+
100+
def _is_valid_polygon(hull: list[Point]) -> bool:
101+
"""Check if hull forms a valid polygon (has at least one non-collinear turn)."""
102+
for i in range(len(hull)):
103+
p1 = hull[i]
104+
p2 = hull[(i + 1) % len(hull)]
105+
p3 = hull[(i + 2) % len(hull)]
106+
if abs(_cross_product(p1, p2, p3)) > 1e-9:
107+
return True
108+
return False
109+
110+
111+
def _add_point_to_hull(hull: list[Point], point: Point) -> None:
112+
"""Add a point to hull, removing collinear intermediate points."""
113+
last = len(hull) - 1
114+
if len(hull) > 1 and _is_point_on_segment(
115+
hull[last - 1], hull[last], point
116+
):
117+
hull[last] = Point(point.x, point.y)
118+
else:
119+
hull.append(Point(point.x, point.y))
120+
121+
71122
def jarvis_march(points: list[Point]) -> list[Point]:
72123
"""
73124
Find the convex hull of a set of points using the Jarvis March algorithm.
74125
75-
The algorithm starts with the leftmost point and wraps around the set of points,
76-
selecting the most counter-clockwise point at each step.
126+
The algorithm starts with the leftmost point and wraps around the set of
127+
points, selecting the most counter-clockwise point at each step.
77128
78129
Args:
79130
points: List of Point objects representing 2D coordinates
@@ -93,87 +144,41 @@ def jarvis_march(points: list[Point]) -> list[Point]:
93144

94145
convex_hull: list[Point] = []
95146

96-
# Find the leftmost point (and bottom-most in case of tie)
97-
left_point_idx = 0
98-
for i in range(1, len(unique_points)):
99-
if unique_points[i].x < unique_points[left_point_idx].x or (
100-
unique_points[i].x == unique_points[left_point_idx].x
101-
and unique_points[i].y < unique_points[left_point_idx].y
102-
):
103-
left_point_idx = i
104-
147+
# Find the leftmost point
148+
left_point_idx = _find_leftmost_point(unique_points)
105149
convex_hull.append(
106150
Point(unique_points[left_point_idx].x, unique_points[left_point_idx].y)
107151
)
108152

109153
current_idx = left_point_idx
110154
while True:
111155
# Find the next counter-clockwise point
112-
next_idx = (current_idx + 1) % len(unique_points)
113-
# Make sure next_idx is not the same as current_idx (handle duplicates)
114-
while next_idx == current_idx:
115-
next_idx = (next_idx + 1) % len(unique_points)
116-
117-
for i in range(len(unique_points)):
118-
# Skip the current point itself (handles duplicates)
119-
if i == current_idx:
120-
continue
121-
if (
122-
_cross_product(
123-
unique_points[current_idx],
124-
unique_points[i],
125-
unique_points[next_idx],
126-
)
127-
> 0
128-
):
129-
next_idx = i
156+
next_idx = _find_next_hull_point(unique_points, current_idx)
130157

131158
if next_idx == left_point_idx:
132-
# Completed constructing the hull
133159
break
134160

135-
# Safety check: if next_idx == current_idx, we have duplicates causing issues
136161
if next_idx == current_idx:
137162
break
138163

139164
current_idx = next_idx
165+
_add_point_to_hull(convex_hull, unique_points[current_idx])
140166

141-
# Check if the last point is collinear with new point and second-to-last
142-
last = len(convex_hull) - 1
143-
if len(convex_hull) > 1 and _is_point_on_segment(
144-
convex_hull[last - 1], convex_hull[last], unique_points[current_idx]
145-
):
146-
# Remove the last point from the hull
147-
convex_hull[last] = Point(
148-
unique_points[current_idx].x, unique_points[current_idx].y
149-
)
150-
else:
151-
convex_hull.append(
152-
Point(unique_points[current_idx].x, unique_points[current_idx].y)
153-
)
154-
155-
# Check for edge case: last point collinear with first and second-to-last
167+
# Check for degenerate cases
156168
if len(convex_hull) <= 2:
157169
return []
158170

171+
# Check if last point is collinear with first and second-to-last
159172
last = len(convex_hull) - 1
160-
if _is_point_on_segment(convex_hull[last - 1], convex_hull[last], convex_hull[0]):
173+
if _is_point_on_segment(
174+
convex_hull[last - 1], convex_hull[last], convex_hull[0]
175+
):
161176
convex_hull.pop()
162177
if len(convex_hull) == 2:
163178
return []
164179

165-
# Final check: verify the hull forms a valid polygon (at least one non-zero cross product)
166-
# If all cross products are zero, all points are collinear
167-
has_turn = False
168-
for i in range(len(convex_hull)):
169-
p1 = convex_hull[i]
170-
p2 = convex_hull[(i + 1) % len(convex_hull)]
171-
p3 = convex_hull[(i + 2) % len(convex_hull)]
172-
if abs(_cross_product(p1, p2, p3)) > 1e-9:
173-
has_turn = True
174-
break
175-
176-
if not has_turn:
180+
# Verify the hull forms a valid polygon
181+
if not _is_valid_polygon(convex_hull):
177182
return []
178183

179184
return convex_hull

0 commit comments

Comments
 (0)