Skip to content

Commit f25262b

Browse files
committed
#25 Polygon from lists of points, CW/CCW checking
1 parent 53bf5ee commit f25262b

File tree

3 files changed

+131
-30
lines changed

3 files changed

+131
-30
lines changed

src/uavfpy/planner/coverage/bdc.py

-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from polyskel import polyskel
21
import networkx as nx
32
import numpy as np
43
import copy, enum
@@ -47,7 +46,6 @@ def degree2rad(deg):
4746
"""
4847
return deg * np.pi / 180
4948

50-
5149
def discretize_entire(J: nx.DiGraph, R: nx.Graph, gridsz: float):
5250
pts = get_points_array(J)
5351
xmin, xmax = np.min(pts[:, 0]), np.max(pts[:, 0])

src/uavfpy/planner/coverage/polygon.py

+82-24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import networkx as nx
44
from matplotlib import pyplot as plt
55
import copy
6+
import enum
7+
8+
9+
class ORIENTATION(enum.Enum):
10+
CCW = 1
11+
CW = -1
612

713

814
def beta_clusters(
@@ -41,20 +47,56 @@ def dist(p1, p2):
4147
return points
4248

4349

44-
class RandomPolygon(object):
45-
def __init__(self, n, points=None, holes=2):
46-
if points is not None:
47-
self.points = points
48-
else:
49-
self.points = beta_clusters(clusters=4, ppc=n // 4)
50-
51-
self.nholes = holes
52-
53-
self.G, self.dt, self.dt_orig = self.polygon(
54-
self.points,
55-
holes=holes,
56-
removals=n // 3,
57-
)
50+
def reordercw(points: np.ndarray, tobe: str):
51+
"""return numpy view of re-ordered points"""
52+
cw = 0.0
53+
for i in range(points.shape[0] - 1):
54+
p1, p2 = points[i], points[i + 1]
55+
cw += p2[0] * p1[1] - p1[0] * p2[1]
56+
if cw >= 0 and tobe == ORIENTATION.CW:
57+
return points
58+
elif cw < 0 and tobe == ORIENTATION.CW:
59+
print("reversing")
60+
return points[::-1]
61+
elif cw <= 0 and tobe == ORIENTATION.CCW:
62+
return points
63+
elif cw > 0 and tobe == ORIENTATION.CCW:
64+
print("reversing")
65+
return points[::-1]
66+
67+
68+
class Polygon(object):
69+
def __init__(self, boundaries: np.ndarray, holes: list):
70+
if boundaries.shape[0] < 3:
71+
raise ValueError("Boundaries must have at least 3 points.")
72+
73+
# check orientation of inputs
74+
boundaries = reordercw(boundaries, ORIENTATION.CW)
75+
for i, hole in enumerate(holes):
76+
holes[i] = reordercw(hole, ORIENTATION.CCW)
77+
78+
# create digraph from list
79+
self.G = nx.DiGraph()
80+
node = 0
81+
# add all points
82+
for i in range(boundaries.shape[0] - 1):
83+
self.G.add_node(node, points=boundaries[i])
84+
self.G.add_edge(node, node + 1, weight=1)
85+
node += 1
86+
# add edge to close the loop and add last node
87+
self.G.add_node(node, points=boundaries[-1])
88+
self.G.add_edge(node, 0, weight=1)
89+
node += 1
90+
for hole in holes:
91+
hole0 = node
92+
for i in range(hole.shape[0] - 1):
93+
self.G.add_node(node, points=hole[i])
94+
self.G.add_edge(node, node + 1, weight=2)
95+
node += 1
96+
# add edge to close the loop and add last node
97+
self.G.add_node(node, points=hole[-1])
98+
self.G.add_edge(node, hole0, weight=2)
99+
node += 1
58100

59101
def removable_interiors(self, dt: spatial.Delaunay) -> tuple:
60102
"""find indices of interior simplices that are safe to remove in dt"""
@@ -220,12 +262,12 @@ def polygon(
220262
for (e1, e2), c in zip(M.edges, cyc):
221263
outer = True
222264
M[e1][e2]["weight"] = 1
223-
cw += self.addcw(H, e1, e2)
265+
cw += self.addcw_h(H, e1, e2)
224266
else:
225267
for (e1, e2), c in zip(M.edges, cyc):
226268
M[e1][e2]["weight"] = 2
227269
outer = False
228-
cw += self.addcw(H, e1, e2)
270+
cw += self.addcw_h(H, e1, e2)
229271
cw = cw >= 0
230272
# categorize nodes
231273
# append
@@ -240,7 +282,7 @@ def polygon(
240282
out_graph = nx.compose_all(outputgraphs)
241283
return out_graph, dt, dt_orig
242284

243-
def addcw(self, H: nx.DiGraph, e1: int, e2: int) -> float:
285+
def addcw_h(self, H: nx.DiGraph, e1: int, e2: int) -> float:
244286
"""determine which way the edge is pointing
245287
246288
e.g.
@@ -257,9 +299,11 @@ def addcw(self, H: nx.DiGraph, e1: int, e2: int) -> float:
257299
p1, p2 = H.nodes[e1]["points"], H.nodes[e2]["points"]
258300
return (p2[0] - p1[0]) * (p2[1] + p1[1])
259301

302+
def cw(self, p1, p2) -> float:
303+
return (p2[0] - p1[0]) * (p2[1] + p1[1])
304+
260305
def plot(
261306
self,
262-
G: nx.DiGraph,
263307
ax: plt.Axes,
264308
posattr: str = "points",
265309
arrows: bool = False,
@@ -273,14 +317,14 @@ def plot(
273317
draw_nodes=True,
274318
) -> plt.Axes:
275319
"""Draw a DiGraph `G` with points stored in `posattr` onto `ax`"""
276-
pos = nx.get_node_attributes(G, posattr)
320+
pos = nx.get_node_attributes(self.G, posattr)
277321
if not ecolor:
278322
try:
279-
ecolor = [G[u][v][ecolorattr] for u, v in G.edges()]
323+
ecolor = [self.G[u][v][ecolorattr] for u, v in self.G.edges()]
280324
except:
281-
ecolor = [5 for _ in G.edges()]
325+
ecolor = [5 for _ in self.G.edges()]
282326
nx.draw_networkx_edges(
283-
G,
327+
self.G,
284328
pos,
285329
ax=ax,
286330
node_size=4,
@@ -291,20 +335,34 @@ def plot(
291335
)
292336
if draw_nodes:
293337
nx.draw_networkx_nodes(
294-
G,
338+
self.G,
295339
pos,
296340
ax=ax,
297341
node_shape=m,
298342
node_color=nodecolor,
299343
node_size=30,
300344
)
301345
if node_text:
302-
for n in G.nodes:
346+
for n in self.G.nodes:
303347
ax.text(pos[n][0], pos[n][1], s=str(n))
304348
ax.autoscale(tight=False)
305349
return ax
306350

307351

352+
class RandomPolygon(Polygon):
353+
def __init__(self, n, points=None, holes=2):
354+
if points is not None:
355+
self.points = points
356+
else:
357+
self.points = beta_clusters(clusters=4, ppc=n // 4)
358+
self.nholes = holes
359+
self.G, self.dt, self.dt_orig = self.polygon(
360+
self.points,
361+
holes=holes,
362+
removals=n // 3,
363+
)
364+
365+
308366
def stupid_spiky_polygon(
309367
R_inner: int,
310368
R_outer: int,

tests/coverage/test_polygon.py

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
11
import pytest
22
from uavfpy.planner.coverage import polygon
33
import networkx as nx
4+
import numpy as np
5+
6+
# a boundary whose points are oriented clockwise
7+
CW = [
8+
[0.1, 0.2],
9+
[0.6, 0.6],
10+
[0.8, 0.1],
11+
[0.3, -0.5],
12+
[0.15, -0.1],
13+
]
14+
15+
# a hole whose points are oriented clockwise
16+
CW_HOLE = [
17+
[0.2, 0.2],
18+
[0.4, 0.25],
19+
[0.3, 0.0],
20+
]
21+
422

523
@pytest.fixture()
624
def rand_poly():
7-
return polygon.RandomPolygon(50, holes= 5)
25+
return polygon.RandomPolygon(50, holes=5)
26+
827

928
def test_beta_clusters():
1029
npoints = 50
11-
points = polygon.beta_clusters(5, ppc = npoints // 5)
30+
points = polygon.beta_clusters(5, ppc=npoints // 5)
1231
assert points.shape[0] == npoints
1332
assert points.shape[1] == 2
1433

34+
1535
def test_make_rand_poly(rand_poly):
1636
assert isinstance(rand_poly.G, nx.DiGraph)
1737

38+
1839
def test_randpoly_properties(rand_poly):
1940
# planarity
2041
assert nx.is_planar(rand_poly.G)
21-
42+
2243
# connectivity
2344
weakly_connected_parts = list(nx.weakly_connected_components(rand_poly.G))
2445
if rand_poly.nholes == 0:
@@ -27,4 +48,28 @@ def test_randpoly_properties(rand_poly):
2748
assert len(weakly_connected_parts) == rand_poly.nholes + 1
2849

2950
# the graph contains one cycle for the outer boundary, and one cycle for each hole
30-
assert len(list(nx.simple_cycles(rand_poly.G))) == rand_poly.nholes + 1
51+
assert len(list(nx.simple_cycles(rand_poly.G))) == rand_poly.nholes + 1
52+
53+
54+
@pytest.fixture()
55+
def boundaries_cw():
56+
boundaries = np.array(CW)
57+
return boundaries
58+
59+
60+
@pytest.fixture()
61+
def boundaries_ccw():
62+
boundaries = np.array(list(reversed(CW)))
63+
return boundaries
64+
65+
66+
@pytest.fixture()
67+
def holes_cw():
68+
holes = [np.array(CW_HOLE)]
69+
return holes
70+
71+
72+
@pytest.fixture()
73+
def holes_ccw():
74+
holes = [np.array(list(reversed(CW_HOLE)))]
75+
return holes

0 commit comments

Comments
 (0)