Skip to content

Commit

Permalink
add tests for circle calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
riedmaph committed Mar 21, 2019
1 parent 18dc5bb commit a53aaa9
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 28 deletions.
32 changes: 30 additions & 2 deletions populartimes/crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ def __init__(self, expression, message):
self.message = message


def rect_circle_collision(rect_left, rect_right, rect_bottom, rect_top, circle_x, circle_y, radius):
# returns true iff circle intersects rectangle

def clamp(val, min, max):
# limits value to the range min..max
if val < min:
return min
if val > max:
return max
return val

# Find the closest point to the circle within the rectangle
closest_x = clamp(circle_x, rect_left, rect_right);
closest_y = clamp(circle_y, rect_bottom, rect_top);

# Calculate the distance between the circle's center and this closest point
dist_x = circle_x - closest_x;
dist_y = circle_y - closest_y;

# If the distance is less than the circle's radius, an intersection occurs
dist_sq = (dist_x * dist_x) + (dist_y * dist_y);

return dist_sq < (radius * radius);

def cover_rect_with_cicles(w, h, r):
"""
fully cover a rectangle of given width and height with
Expand Down Expand Up @@ -81,6 +105,10 @@ def cover_rect_with_cicles(w, h, r):
for x in range(cnt_x):
res.append((x_offs + x*x_dist, y_offs + y*y_dist))

# top-right circle is not always required
if res and not rect_circle_collision(0, w, 0, h, res[-1][0], res[-1][1], r):
res = res[0:-1]

return res

def get_circle_centers(b1, b2, radius):
Expand All @@ -97,8 +125,8 @@ def get_circle_centers(b1, b2, radius):
ne = Point(b2)

# north/east distances
dist_lat = int(vincenty(Point(sw[0], sw[1]), Point(ne[0], sw[1])).meters)
dist_lng = int(vincenty(Point(sw[0], sw[1]), Point(sw[0], ne[1])).meters)
dist_lat = vincenty(Point(sw[0], sw[1]), Point(ne[0], sw[1])).meters
dist_lng = vincenty(Point(sw[0], sw[1]), Point(sw[0], ne[1])).meters

circles = cover_rect_with_cicles(dist_lat, dist_lng, radius)
cords = [
Expand Down
63 changes: 63 additions & 0 deletions tests/test_cover_rect_with_cicles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from populartimes.crawler import cover_rect_with_cicles, rect_circle_collision
import random
from math import pi

def generate_testcases():
for w in [0.1, 1, 2, 2.1, 5, 7, 10]:
for h in [0.08, 0.1, 0.2, 1, 1.5, 5, 6, 8]:
for r in [0.1, 0.6, 1, 2, 4, 5, 100]:
circles = cover_rect_with_cicles(w, h, r)
yield (w, h, r, circles)

def calc_bounding_box(circles, r):
xs = [c[0] for c in circles]
ys = [c[1] for c in circles]
return ((min(xs) - r, min(ys) - r), (max(xs) + r, max(ys) + r))

def test_cover_rect_with_cicles_all_in():
# test if calculated circles are all at least partly contained in the rect
# (otherwise some circles would be superfluous)
for w, h, r, circles in generate_testcases():
assert all([rect_circle_collision(0,w,0,h,c[0],c[1],r) for c in circles])

def test_cover_rect_with_cicles_coverage():
# test if circles fully cover the rect
for w, h, r, circles in generate_testcases():
# test with 1000 random points
for tst in range(1000):
# choose random point within rect
p = (random.uniform(0,w), random.uniform(0,h))
# check if point is contained in any of the calculated circles
# (distance to center is <= radius)
assert any([(p[0]-c[0])**2 + (p[1]-c[1])**2 <= r**2 for c in circles])

def test_cover_rect_with_cicles_area():
# test if area of returned circles is reaonable compared to rect area
for w, h, r, circles in generate_testcases():
if w > 2*r and h > 2*r:
# calculate bounding box
lower_left, upper_right = calc_bounding_box(circles, r)

area_bounding_box = (upper_right[0] - lower_left[0]) * (upper_right[1] - lower_left[1])
area_circ_total = len(circles) * r * r * pi
area_rect = w * h

# use Monte Carlo method to approximate combined circle area (union of all circles)
# 1000 sample points should give about 99% accuracy
points = [(random.uniform(lower_left[0], upper_right[0]), random.uniform(lower_left[1], upper_right[1])) for tst in range(1000)]
inside = [any([(p[0]-c[0])**2 + (p[1]-c[1])**2 <= r**2 for c in circles]) for p in points]
area_circ = inside.count(True) / len(inside) * area_bounding_box

area_overlap = area_circ_total - area_circ

ratio_circ_rect = area_circ / area_rect
ratio_total = area_circ_total / area_rect
ratio_overlap = area_overlap / area_circ_total

if len(circles) > 1000:
assert(ratio_circ_rect < 1.1) # max 10% outside of rect
assert(ratio_total < 1.3) # max 30% overhead
elif len(circles) > 100:
assert(ratio_circ_rect < 1.3) # max 30% outside of rect
assert(ratio_total < 1.5) # max 50% overhead
assert(ratio_overlap < 0.2) # max 20% overlap
59 changes: 33 additions & 26 deletions tests/test_get_circle_centers.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
from populartimes.crawler import get_circle_centers
import random
from geopy import Point
from geopy.distance import vincenty, VincentyDistance


def test_get_circle_centers():
circle_centers = get_circle_centers(
b1=[48.132986, 11.566126], b2=[48.142199, 11.580047], radius=180
)
def generate_testcases():
# origin (south-west)
sw = [48.132986, 11.566126]
# width, height, radius in meters
for w in [1, 10, 80, 200, 1000, 5000]:
for h in [1, 20, 300, 1000, 20000]:
for r in [180, 500]:
# north-east (se + width/height)
ne = VincentyDistance(meters=w).destination(
VincentyDistance(meters=h)
.destination(point=sw, bearing=90),
bearing=0
)[:2]
circles = get_circle_centers(sw, ne, r)
yield (sw, ne, w, h, r, circles)

assert circle_centers == [
(48.13438792255016, 11.567335135224921),
(48.137191779339744, 11.567335135224921),
(48.139995634754655, 11.567335135224921),
(48.14279948879493, 11.567335135224921),
(48.13298589823778, 11.570962540893714),
(48.135789755714754, 11.570962540893714),
(48.13859361181704, 11.570962540893714),
(48.14139746654468, 11.570962540893714),
(48.13438761726354, 11.574589946541003),
(48.13719147405328, 11.574589946541003),
(48.139995329468334, 11.574589946541003),
(48.14279918350877, 11.574589946541003),
(48.13298536398607, 11.578217352150666),
(48.13578922146331, 11.578217352150666),
(48.138593077565865, 11.578217352150666),
(48.14139693229376, 11.578217352150666),
(48.13438685404702, 11.581844757706575),
(48.13719071083713, 11.581844757706575),
(48.139994566252575, 11.581844757706575),
(48.14279842029337, 11.581844757706575)
]
def test_get_circle_centers():
# test if circles fully cover the rect
for sw, ne, w, h, r, circles in generate_testcases():
# test with 1000 random points
for tst in range(1000):
# choose random point within rect
p = (random.uniform(0,w), random.uniform(0,h))
# translate to lat/lng
pp = VincentyDistance(meters=p[0]).destination(
VincentyDistance(meters=p[1])
.destination(point=sw, bearing=90),
bearing=0
)
# check if point is contained in any of the calculated circles
assert any([vincenty(pp, Point(c[0], c[1])).meters <= r for c in circles])

0 comments on commit a53aaa9

Please sign in to comment.