diff --git a/cyaron/__init__.py b/cyaron/__init__.py index 3ae4490..2864741 100644 --- a/cyaron/__init__.py +++ b/cyaron/__init__.py @@ -11,5 +11,6 @@ from .utils import * from .consts import * from .vector import Vector +from .polygon import Polygon from random import randint, randrange, uniform, choice, random diff --git a/cyaron/polygon.py b/cyaron/polygon.py new file mode 100644 index 0000000..cedff9b --- /dev/null +++ b/cyaron/polygon.py @@ -0,0 +1,167 @@ +from .utils import * +from .consts import * +import random +import math + +class Polygon: + def __init__(self,points=[]): + if not list_like(points): + raise Exception("polygon must be constructed by a list of points") + self.points = points + + def __str__(self): + buf = [] + for point in self.points: + buf.append(str(point[0]) + " " + str(point[1])) + return '\n'.join(buf) + + def perimeter(self): + ans = 0 + for i in range(0, len(self.points)): + a = self.points[i] + b = self.points[(i + 1) % len(self.points)] + ans = ans + math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + + (a[1] - b[1]) * (a[1] - b[1])) + return ans + + def area(self): + ans = 0 + for i in range(0, len(self.points)): + a = self.points[i] + b = self.points[(i + 1) % len(self.points)] + ans = ans + a[0] * b[1] - a[1] * b[0] + if ans < 0: + ans = -ans + ans = ans / 2 + return ans + + #generate a convex hull with n points + #it's possible to have even edges + @staticmethod + def convex_hull(n, **kwargs): + # fx, fy are functions which map [0,1] to int or float + fx = kwargs.get("fx", lambda x: x) + fy = kwargs.get("fy", lambda x: x) + sz = n * 2 + result = [] + while len(result) < n: + points = [] + # about 10 points will be randomized + randomize_prob = int(sz / 10) + 1 + if randomize_prob < 10: + randomize_prob = 10 + for i in range(0, sz): + angle = random.uniform(0, 2 * PI) + x = 0.5 + math.cos(angle) * 0.48 + y = 0.5 + math.sin(angle) * 0.48 + if random.randint(0, randomize_prob) == 0: + x = x + random.uniform(-0.005, 0.005) + y = y + random.uniform(-0.005, 0.005) + points.append([fx(x), fy(y)]) + # compute convex hull for points and store in rst + points = sorted(points) + # unique + tmp = [] + for i in range(0, len(points)): + if i == 0 or points[i - 1] != points[i]: + tmp.append(points[i]) + points = tmp + st = [] # stack + for i in range(0, len(points)): + while len(st) >= 2: + a = st[len(st) - 1] + b = points[i] + o = st[len(st) - 2] + if (a[0] - o[0]) * (b[1] - o[1]) - \ + (a[1] - o[1]) * (b[0] - o[0]) >= 0: + break + st.pop() + st.append(points[i]) + g = len(st) + 1 + for i in range(0, len(points) - 1)[::-1]: + while len(st) >= g: + a = st[len(st) - 1] + b = points[i] + o = st[len(st) - 2] + if (a[0] - o[0]) * (b[1] - o[1]) - \ + (a[1] - o[1]) * (b[0] - o[0]) >= 0: + break + st.pop() + st.append(points[i]) + st.pop() + result = st + sz = int(sz * 1.7) + 3 # if failed, increase size and try again + ids = [i for i in range(0, len(result))] + random.shuffle(ids) + ids = ids[0:n] + ids = sorted(ids) + output = [result[ids[i]] for i in range(0, n)] + poly = Polygon(output) + return poly + + # find a path from points[0] to points[1] and cross all points in [points] + @staticmethod + def __conquer(points): + if len(points) <= 2: + return points + if len(points) == 3: + (points[1],points[2])=(points[2],points[1]) + return points + divide_id = random.randint(2, len(points) - 1) + divide_point1 = points[divide_id] + divide_k = random.uniform(0.01, 0.99) + divide_point2 = [divide_k * (points[1][0] - points[0][0]) + points[0][0], + divide_k * (points[1][1] - points[0][1]) + points[0][1]] + # path: points[0]->points[divide]->points[1] + # dividing line in the form Ax+By+C=0 + divide_line = [divide_point2[1] - divide_point1[1], + divide_point1[0] - divide_point2[0], + -divide_point1[0] * divide_point2[1] + + divide_point1[1] * divide_point2[0]] + p0 = (divide_line[0] * points[0][0] + divide_line[1] * points[0][1] + divide_line[2] >= 0) + p1 = (divide_line[0] * points[1][0] + divide_line[1] * points[1][1] + divide_line[2] >= 0) + if p0 == p1: # the divide point isn't good enough... + return Polygon.__conquer(points) + s = [[], []] + s[p0].append(points[0]) + s[p0].append(divide_point1) + s[not p0].append(divide_point1) + s[not p0].append(points[1]) + for i in range(2, len(points)): + if i == divide_id: + continue + pt = (divide_line[0] * points[i][0] + divide_line[1] * points[i][1] + divide_line[2] >= 0) + s[pt].append(points[i]) + pa = Polygon.__conquer(s[p0]) + pb = Polygon.__conquer(s[not p0]) + pb.pop(0) + return pa + pb + + # generate simple polygon from given points (int[2] or float[2]) + # O(nlogn)~O(n^2) + @staticmethod + def simple_polygon(points): + if not list_like(points): + raise Exception("source point is not a list") + random.shuffle(points) + if len(points) < 3: + return points + # divide by points[0], points[1] + divide_line = [points[1][1] - points[0][1], + points[0][0] - points[1][0], + -points[0][0] * points[1][1] + + points[0][1] * points[1][0]] + s = [[], []] + s[0].append(points[0]) + s[0].append(points[1]) + s[1].append(points[1]) + s[1].append(points[0]) + for i in range(2, len(points)): + pt = (divide_line[0] * points[i][0] + divide_line[1] * points[i][1] + divide_line[2] >= 0) + s[pt].append(points[i]) + pa = Polygon.__conquer(s[0]) + pb = Polygon.__conquer(s[1]) + pa.pop(0) + pb.pop(0) + poly = Polygon(pa + pb) + return poly diff --git a/cyaron/tests/__init__.py b/cyaron/tests/__init__.py index d4fd8cf..d61c4c8 100644 --- a/cyaron/tests/__init__.py +++ b/cyaron/tests/__init__.py @@ -1,3 +1,4 @@ from .sequence_test import TestSequence from .io_test import TestIO -from .str_test import TestString \ No newline at end of file +from .str_test import TestString +from .polygon_test import TestPolygon \ No newline at end of file diff --git a/cyaron/tests/polygon_test.py b/cyaron/tests/polygon_test.py new file mode 100644 index 0000000..ff621f4 --- /dev/null +++ b/cyaron/tests/polygon_test.py @@ -0,0 +1,58 @@ +import unittest +from cyaron import Polygon,Vector +class TestPolygon(unittest.TestCase): + def test_convex_hull(self): + hull = Polygon.convex_hull(300, fx=lambda x:int(x*100000), fy=lambda x:int(x*100000)) + points = hull.points + points = sorted(points) + # unique + tmp = [] + for i in range(0, len(points)): + if i == 0 or points[i - 1] != points[i]: + tmp.append(points[i]) + points = tmp + st = [] # stack + for i in range(0, len(points)): + while len(st) >= 2: + a = st[len(st) - 1] + b = points[i] + o = st[len(st) - 2] + if (a[0] - o[0]) * (b[1] - o[1]) - \ + (a[1] - o[1]) * (b[0] - o[0]) >= 0: + break + st.pop() + st.append(points[i]) + g = len(st) + 1 + for i in range(0, len(points) - 1)[::-1]: + while len(st) >= g: + a = st[len(st) - 1] + b = points[i] + o = st[len(st) - 2] + if (a[0] - o[0]) * (b[1] - o[1]) - \ + (a[1] - o[1]) * (b[0] - o[0]) >= 0: + break + st.pop() + st.append(points[i]) + st.pop() + self.assertEqual(len(st), len(hull.points)) + def test_perimeter_area(self): + poly = Polygon([[0,0],[0,1],[1,1],[1,0]]) + self.assertEqual(poly.perimeter(),4) + self.assertEqual(poly.area(),1) + def test_simple_polygon(self): + poly = Polygon.simple_polygon(Vector.random(300, [1000, 1000])) + points = poly.points + for i in range(0,len(points)): + for j in range(i+2,len(points)): + if j==len(points)-1 and i==0: + continue + a=points[i] + b=points[(i+1)%len(points)] + c=points[j] + d=points[(j+1)%len(points)] + prod=lambda x,y: x[0]*y[1]-x[1]*y[0] + t1=prod([c[0]-a[0],c[1]-a[1]],[d[0]-a[0],d[1]-a[1]])\ + *prod([c[0]-b[0],c[1]-b[1]],[d[0]-b[0],d[1]-b[1]]) + t2=prod([a[0]-c[0],a[1]-c[1]],[b[0]-c[0],b[1]-c[1]])\ + *prod([a[0]-d[0],a[1]-d[1]],[b[0]-d[0],b[1]-d[1]]) + self.assertFalse(t1<=1e-9 and t2<=1e-9) diff --git a/examples/test_polygon.py b/examples/test_polygon.py new file mode 100644 index 0000000..9355788 --- /dev/null +++ b/examples/test_polygon.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +from cyaron import * +print "random convex hull of size 300:" +hull=Polygon.convex_hull(300, fx=lambda x:int(x*1000), fy=lambda x:int(x*1000)) +print hull +print "perimeter:" +print hull.perimeter() +print "area:" +print hull.area() +points = Vector.random(300, [1000, 1000]) +print "random simple polygon of size 300:" +poly = Polygon.simple_polygon(points) +print poly +print "perimeter:" +print poly.perimeter() +print "area:" +print poly.area()