-
-
Notifications
You must be signed in to change notification settings - Fork 50k
feat: add Jarvis March (Gift Wrapping) convex hull algorithm #14225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f347c7f
7028fa7
5460dca
938e20b
c2c2e75
a376a44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| """ | ||
| Jarvis March (Gift Wrapping) algorithm for finding the convex hull of a set of points. | ||
|
|
||
| The convex hull is the smallest convex polygon that contains all the points. | ||
|
|
||
| Time Complexity: O(n*h) where n is the number of points and h is the number of | ||
| hull points. | ||
| Space Complexity: O(h) where h is the number of hull points. | ||
|
|
||
| USAGE: | ||
| -> Import this file into your project. | ||
| -> Use the jarvis_march() function to find the convex hull of a set of points. | ||
| -> Parameters: | ||
| -> points: A list of Point objects representing 2D coordinates | ||
|
|
||
| REFERENCES: | ||
| -> Wikipedia reference: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm | ||
| -> GeeksforGeeks: https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/ | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| class Point: | ||
| """Represents a 2D point with x and y coordinates.""" | ||
|
|
||
| def __init__(self, x_coordinate: float, y_coordinate: float) -> None: | ||
| self.x = x_coordinate | ||
| self.y = y_coordinate | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| if not isinstance(other, Point): | ||
| return NotImplemented | ||
| return self.x == other.x and self.y == other.y | ||
|
|
||
| def __repr__(self) -> str: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no test file in this pull request nor any test function or class in the file |
||
| return f"Point({self.x}, {self.y})" | ||
|
|
||
| def __hash__(self) -> int: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no test file in this pull request nor any test function or class in the file |
||
| return hash((self.x, self.y)) | ||
|
|
||
|
|
||
| def _cross_product(origin: Point, point_a: Point, point_b: Point) -> float: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no test file in this pull request nor any test function or class in the file |
||
| """ | ||
| Calculate the cross product of vectors OA and OB. | ||
|
|
||
| Returns: | ||
| > 0: Counter-clockwise turn (left turn) | ||
| = 0: Collinear | ||
| < 0: Clockwise turn (right turn) | ||
| """ | ||
| return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * ( | ||
| point_b.x - origin.x | ||
| ) | ||
|
|
||
|
|
||
| def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no test file in this pull request nor any test function or class in the file |
||
| """Check if a point lies on the line segment between p1 and p2.""" | ||
| # Check if point is collinear with segment endpoints | ||
| cross = (point.y - p1.y) * (p2.x - p1.x) - (point.x - p1.x) * (p2.y - p1.y) | ||
|
|
||
| if abs(cross) > 1e-9: | ||
| return False | ||
|
|
||
| # Check if point is within the bounding box of the segment | ||
| return min(p1.x, p2.x) <= point.x <= max(p1.x, p2.x) and min( | ||
| p1.y, p2.y | ||
| ) <= point.y <= max(p1.y, p2.y) | ||
|
|
||
|
|
||
| def jarvis_march(points: list[Point]) -> list[Point]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no test file in this pull request nor any test function or class in the file |
||
| """ | ||
| Find the convex hull of a set of points using the Jarvis March algorithm. | ||
|
|
||
| The algorithm starts with the leftmost point and wraps around the set of points, | ||
| selecting the most counter-clockwise point at each step. | ||
|
|
||
| Args: | ||
| points: List of Point objects representing 2D coordinates | ||
|
|
||
| Returns: | ||
| List of Points that form the convex hull in counter-clockwise order. | ||
| Returns empty list if there are fewer than 3 non-collinear points. | ||
| """ | ||
| if len(points) <= 2: | ||
| return [] | ||
|
|
||
| convex_hull: list[Point] = [] | ||
|
|
||
| # Find the leftmost point (and bottom-most in case of tie) | ||
| left_point_idx = 0 | ||
| for i in range(1, len(points)): | ||
| if points[i].x < points[left_point_idx].x or ( | ||
| points[i].x == points[left_point_idx].x | ||
| and points[i].y < points[left_point_idx].y | ||
| ): | ||
| left_point_idx = i | ||
|
|
||
| convex_hull.append(Point(points[left_point_idx].x, points[left_point_idx].y)) | ||
|
|
||
| current_idx = left_point_idx | ||
| while True: | ||
| # Find the next counter-clockwise point | ||
| next_idx = (current_idx + 1) % len(points) | ||
| for i in range(len(points)): | ||
| if _cross_product(points[current_idx], points[i], points[next_idx]) > 0: | ||
| next_idx = i | ||
|
|
||
| if next_idx == left_point_idx: | ||
| # Completed constructing the hull | ||
| break | ||
|
|
||
| current_idx = next_idx | ||
|
|
||
| # Check if the last point is collinear with new point and second-to-last | ||
| last = len(convex_hull) - 1 | ||
| if len(convex_hull) > 1 and _is_point_on_segment( | ||
| convex_hull[last - 1], convex_hull[last], points[current_idx] | ||
| ): | ||
| # Remove the last point from the hull | ||
| convex_hull[last] = Point(points[current_idx].x, points[current_idx].y) | ||
| else: | ||
| convex_hull.append(Point(points[current_idx].x, points[current_idx].y)) | ||
|
|
||
| # Check for edge case: last point collinear with first and second-to-last | ||
| if len(convex_hull) <= 2: | ||
| return [] | ||
|
|
||
| last = len(convex_hull) - 1 | ||
| if _is_point_on_segment(convex_hull[last - 1], convex_hull[last], convex_hull[0]): | ||
| convex_hull.pop() | ||
| if len(convex_hull) == 2: | ||
| return [] | ||
|
|
||
| return convex_hull | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| # Example usage | ||
| points = [Point(0, 0), Point(1, 1), Point(0, 1), Point(1, 0), Point(0.5, 0.5)] | ||
| hull = jarvis_march(points) | ||
| print(f"Convex hull: {hull}") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As there is no test file in this pull request nor any test function or class in the file
geometry/jarvis_march.py, please provide doctest for the function__eq__