def test_optimise_point_to_line(self): """ test optimise_point_to_line """ raw_map = raw_map.RawMap(10, 10, 100 * [0]) area_map = area_map.AreaMap(raw_map, lambda _: True) p = vector.PointF(3, 4) a = vector.PointF(2, 0) b = vector.PointF(4, 0) p0 = a # direct path p->(3,0) is valid and optimal self.assertEqual(area_map.optimise_point_to_line(p, p0, a, b), vector.PathF([p, vector.PointF(3, 0)])) # direct path p->a is valid and optimal p = vector.PointF(1, 4) self.assertEqual(area_map.optimise_point_to_line(p, p0, a, b), vector.PathF([p, a])) # direct path p->b is valid and optimal p = vector.PointF(5, 4) self.assertEqual(area_map.optimise_point_to_line(p, p0, a, b), vector.PathF([p, b])) print("h") # direct path p->b is not valid anymore, but # p->(3,2)->(3,0) is valid and optimal. # also note that p->a is valid. area_map[vector.GridTile(3, 1)] = 0 # 3,1 is now unpassable p = vector.PointF(4, 4) self.assertEqual( area_map.optimise_point_to_line(p, p0, a, b), vector.PathF([p, vector.PointF(3, 2), vector.PointF(3, 0)]))
def test_find_obstruction_when_transforming_line(self): """ test find_obstruction_when_transforming_line """ raw_map = raw_map.RawMap(10, 10, 100 * [0]) area_map = area_map.AreaMap(raw_map, lambda _: True) base = vector.PointF(2, 2) start = vector.PointF(8, 2) end = vector.PointF(2, 8) # print(area_map.find_tiles_in_triangle(base, start, end)) self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (1.0, None)) area_map[vector.GridTile(4, 4)] = 0 # 4,4 is now unpassable self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (0.4, vector.GridPoint(5, 4))) area_map[vector.GridTile(6, 3)] = 0 # 6,3 is now unpassable self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (1. / 6., vector.GridTile(7, 3))) area_map[vector.GridTile(4, 2)] = 0 # 4,2 is now unpassable self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (0., vector.GridPoint(5, 2))) area_map[vector.GridTile(2, 2)] = 0 # 2,2 is now unpassable self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (0., vector.GridPoint(5, 2))) # test angle > 90 base = vector.PointF(6, 4) start = vector.PointF(9, 5) end = vector.PointF(3, 5) self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (2. / 3., vector.GridPoint(5, 5))) base = vector.PointF(3, 3) start = vector.PointF(0, 5) end = vector.PointF(9, 7) self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (0.5, vector.GridPoint(4, 5))) area_map[vector.GridTile(6, 5)] = 0 # 6,5 is now unpassable self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (0.5, vector.GridPoint(4, 5))) area_map[vector.GridTile(4, 4)] = -1 # 4,4 is now passable again self.assertEqual( area_map.find_obstruction_when_transforming_line(base, start, end), (5. / 7., vector.GridPoint(6, 6)))
def _create_area_map(self): """ create it """ # create shortcuts width, height = self.raw_map.width, self.raw_map.height # initialise id first free id area_id = self.no_area_id + 1 # # special case the map boundary since we have to understand the boundary edges # in particular the boundary will have area_id 'self.no_area_id + 1' # for x in range(width): # y = 0 line t = vector.GridTile(x, 0) self.__process_tile(area_id, t) if self.pass_test(self.raw_map[t]): p = vector.GridPoint(x, 0) p_xpp = vector.GridPoint(x + 1, 0) # inner side is to the right self.edges[area_id].append(vector.GridEdge(p, p_xpp)) # y = max line t = vector.GridTile(x, height - 1) self.__process_tile(area_id, t) if self.pass_test(self.raw_map[t]): p = vector.GridPoint(x, height) p_xpp = vector.GridPoint(x + 1, height) # inner side is to the right self.edges[area_id].append(vector.GridEdge(p_xpp, p)) for y in range(height): # x = 0 line t = vector.GridTile(0, y) self.__process_tile(area_id, t) if self.pass_test(self.raw_map[t]): p = vector.GridPoint(0, y, ) p_ypp = vector.GridPoint(0, y + 1) # inner side is to the left self.edges[area_id].append(vector.GridEdge(p_ypp, p)) # x = max line t = vector.GridTile(width - 1, y) self.__process_tile(area_id, t) if self.pass_test(self.raw_map[t]): p = vector.GridPoint(width, y, ) p_ypp = vector.GridPoint(width, y + 1) # inner side is to the left self.edges[area_id].append(vector.GridEdge(p, p_ypp)) # increment area_id area_id += 1 # iterate over all data for t in self.raw_map.tiles_iterator(): if self.__process_tile(area_id, t): # if we found something, increment area_id area_id += 1
def tiles_iterator(self): """ returns an iterator over all tiles """ for x in range(self.width): for y in range(self.height): yield vector.GridTile(x, y)
def find_tiles_in_triangle_iterator(self, a, b, c): """ find all tiles in the triangle defined by the three points """ assert (isinstance(a, vector.PointF)) assert (isinstance(b, vector.PointF)) assert (isinstance(c, vector.PointF)) # tolerance for the conversion to integer values eps = 1e-6 eps_floor = lambda x: math.floor(x + eps) eps_ceil = lambda x: math.ceil(x - eps) # sort the points by ascending y-coordinate p_y_min, p_mid, p_y_max = sorted([a, b, c], key=lambda p: p.y) # special case: empty triangle if p_y_min.y == p_y_max.y: return [] # helper def intersection_with_line(y, a, b): """ intersection of y const with the line a->b if a->b is parallel to the x-axis, the point b is used """ if b.y - a.y != 0.: # careful: the denominator can be zero t = (y - a.y) / (b.y - a.y) return t * (b.x - a.x) + a.x else: # otherwise, a->b is parallel to the x-axis return b.x # iterate over all y between # (largest integer <= p_y_min.y) # to # (smallest integer <= p_y_max.y) # (we use eps here to counter rounding artifacts) i_y_min = eps_floor(p_y_min.y) i_y_max = eps_ceil(p_y_max.y) i_y_mid = eps_ceil(p_mid.y) # special cases: # 1. i_y_min == i_y_mid: handled below # 2. i_y_max == i_y_mid: handled implicitly # 3. i_y_min == i_y_max: cannot happen, excluded above # initialise lower_x_min/max lower_x_min = eps_floor(p_y_min.x) lower_x_max = eps_ceil(p_y_min.x) # special case 1.: p_y_min.y == p_mid.y and both are integers # then we have to add p_mid.x to the range if i_y_min == i_y_mid: lower_x_min = min(lower_x_min, eps_floor(p_mid.x)) lower_x_max = max(lower_x_max, eps_ceil(p_mid.x)) for y in range(i_y_min, i_y_max): # intersection of y+1 const with p_y_min->p_y_max if y + 1 <= p_y_max.y: x1 = intersection_with_line(y + 1, p_y_min, p_y_max) else: # but only if it makes sense x1 = p_y_max.x # find x2 if y + 1 == i_y_mid: # this is the change from p_y_min->p_mid to p_mid->p_y_max x2 = p_mid.x elif y + 1 <= p_mid.y: # we are looking at p_y_min->p_mid x2 = intersection_with_line(y + 1, p_y_min, p_mid) elif y + 1 <= p_y_max.y: # we are looking at p_mid->p_y_max # careful: if the line is parallel to the x-axis, # we want p_mid, hence it comes last x2 = intersection_with_line(y + 1, p_y_max, p_mid) else: x2 = p_y_max.x # rearrange if needed if x2 < x1: x1, x2 = x2, x1 # now find the smallest integer interval containing [x1,x2] upper_x_min, upper_x_max = eps_floor(x1), eps_ceil(x2) # communicate all the tiles between # min(lower_x_min,upper_x_min) # and # max(lower_x_max,upper_x_max) i_min = min(lower_x_min, upper_x_min) i_max = max(lower_x_max, upper_x_max) # communicate the tile for i in range(i_min, i_max): tile = vector.GridTile(i, y) if self.contains(tile): yield tile # swap upper to lower lower_x_min, lower_x_max = upper_x_min, upper_x_max
def optimise_path_loose_ends(self): """ test optimise_path_loose_ends """ raw_map = raw_map.RawMap(10, 10, 100 * [0]) area_map = area_map.AreaMap(raw_map, lambda _: True) start_a = vector.PointF(2, 0) start_b = vector.PointF(4, 0) end_a = vector.PointF(4, 4) end_b = vector.PointF(6, 4) # direct start_b->end_a is valid and optimal path = vector.PathF([start_a, end_b]) self.assertEqual( area_map.optimise_path_loose_ends(path, start_a, start_b, end_a, end_b), vector.PathF([start_b, end_a])) area_map[vector.GridTile(3, 1)] = 0 # 3,1 is now unpassable # direct start_b->end_a is not valid anymore. however # (3,0)->(3,2)->end_a is. note that start_a->end_a is valid path = vector.PathF([start_a, end_a]) self.assertEqual( area_map.optimise_path_loose_ends(path, start_a, start_b, end_a, end_b), vector.PathF([vector.PointF(3, 0), vector.PointF(3, 2), end_a])) area_map[vector.GridTile(3, 1)] = -1 # 3,1 is now passable again # small number eps = 0.001 # slow convergence (fixed by preprocess step) start_a = vector.PointF(2, 0) start_b = vector.PointF(4, eps) end_a = vector.PointF(2, 4) end_b = vector.PointF(4, 4 - eps) # direct start_b->end_b is valid and optimal path = vector.PathF([start_a, end_a]) self.assertEqual( area_map.optimise_path_loose_ends(path, start_a, start_b, end_a, end_b), vector.PathF([start_b, end_b])) area_map[vector.GridTile(2, 1)] = 0 # 2,1 is now unpassable # prevented convergence catastrophe (fixed by preprocess step) start_a = vector.PointF(0, 0) start_b = vector.PointF(10, 1.5) end_a = vector.PointF(0, 3) end_b = vector.PointF(10, 1.5) # optimal is ???->(3,1)->(3,2)->??? path = vector.PathF([start_a, end_a]) self.assertEqual( area_map.optimise_path_loose_ends(path, start_a, start_b, end_a, end_b), vector.PathF([ vector.PointF(860 / 409., 129 / 409.), vector.PointF(2, 1), vector.PointF(2, 2), vector.PointF(860 / 409., 1098 / 409.) ])) # convergence catastrophe (fixed by preprocess step) start_a = vector.PointF(2, 0) start_b = vector.PointF(4, eps) end_a = vector.PointF(2, 2 * eps) end_b = vector.PointF(4, eps) # degerenates to start_b=end_b path = vector.PathF([start_a, end_a]) self.assertEqual( area_map.optimise_path_loose_ends(path, start_a, start_b, end_a, end_b), vector.PathF([start_b, end_b]))