def _one_tangent(points, k1, k2, k3, weight): v1 = points[k2] - points[k1] v2 = points[k3] - points[k2] l1 = vu.naive_length(v1) l2 = vu.naive_length(v2) if weight == 'square': return vu.unit_vector(v1 / (l1 * l1) + v2 / (l2 * l2)) elif weight == 'cube': return vu.unit_vector(v1 / (l1 * l1 * l1) + v2 / (l2 * l2 * l2)) else: # linear weight mode return vu.unit_vector(v1 / l1 + v2 / l2)
def tangents(points, weight='linear', closed=False): """Returns a numpy array of tangent unit vectors for an ordered list of points. arguments: points (numpy float array of shape (N, 3)): points defining a line weight (string, default 'linear'): one of 'linear', 'square' or 'cube', giving increased weight to relatively shorter of 2 line segments at each knot closed (boolean, default False): if True, the points are treated as a closed polyline with regard to end point tangents, otherwise as an open line returns: numpy float array of the same shape as points, containing a unit length tangent vector for each knot (point) note: if two neighbouring points are identical, a divide by zero will occur """ assert points.ndim == 2 and points.shape[1] == 3 assert weight in ['linear', 'square', 'cube'] knot_count = len(points) assert knot_count > 1 tangent_vectors = np.empty((knot_count, 3)) for knot in range(1, knot_count - 1): tangent_vectors[knot] = _one_tangent(points, knot - 1, knot, knot + 1, weight) if closed: assert knot_count > 2, 'closed poly line must contain at least 3 knots for tangent generation' tangent_vectors[0] = _one_tangent(points, -1, 0, 1, weight) tangent_vectors[-1] = _one_tangent(points, -2, -1, 0, weight) else: tangent_vectors[0] = vu.unit_vector(points[1] - points[0]) tangent_vectors[-1] = vu.unit_vector(points[-1] - points[-2]) return tangent_vectors
def segment_normal(self, segment_index): """For a closed polyline return a unit vector giving the 2D (xy) direction of an outward facing normal to a segment.""" successor = self._successor(segment_index) segment_vector = self.coordinates[successor, :2] - self.coordinates[ segment_index, :2] segment_vector = vu.unit_vector(segment_vector) normal_vector = np.zeros(3) normal_vector[0] = -segment_vector[1] normal_vector[1] = segment_vector[0] cw = self.is_clockwise() assert cw is not None, 'polyline is straight' if not cw: normal_vector = -normal_vector return normal_vector
def test_unit_vectors(): v_set = np.array([(3.0, 4.0, 0.0), (3.7, -3.7, 3.7), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0)]) one_over_root_three = 1.0 / maths.sqrt(3.0) expected = np.array([(3.0 / 5.0, 4.0 / 5.0, 0.0), (one_over_root_three, -one_over_root_three, one_over_root_three), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0)]) for v, e in zip(v_set, expected): assert_array_almost_equal(vec.unit_vector(v), e) assert_array_almost_equal(vec.unit_vectors(v_set), expected) azi = [0.0, -90.0, 120.0, 180.0, 270.0, 360.0] expected = np.array([(0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (maths.cos(maths.pi / 6), -0.5, 0.0), (0.0, -1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]) for a, e in zip(azi, expected): assert_array_almost_equal(vec.unit_vector_from_azimuth(a), e)
def _pillar_vector(grid, p_index): # return a unit vector for direction of pillar, in direction of increasing k if np.all(np.isnan(grid.points_cached[:, p_index])): return None k_top = 0 while np.any(np.isnan(grid.points_cached[k_top, p_index])): k_top += 1 k_bot = grid.nk_plus_k_gaps - 1 while np.any(np.isnan(grid.points_cached[k_bot, p_index])): k_bot -= 1 if k_bot == k_top: # following coded to treat None directions as downwards if grid.k_direction_is_down is False: if grid.z_inc_down() is False: return (0.0, 0.0, 1.0) else: return (0.0, 0.0, -1.0) else: if grid.z_inc_down() is False: return (0.0, 0.0, -1.0) else: return (0.0, 0.0, 1.0) else: return vec.unit_vector(grid.points_cached[k_bot, p_index] - grid.points_cached[k_top, p_index])
def voronoi(p, t, b, aoi: rql.Polyline): """Returns dual Voronoi diagram for a Delauney triangulation. arguments: p (numpy float array of shape (N, 2)): seed points used in the Delauney triangulation t (numpy int array of shape (M, 3)): the Delauney triangulation of p as returned by dt() b (numpy int array of shape (B,)): clockwise sorted list of indices into p of the boundary points of the triangulation t aoi (lines.Polyline): area of interest; a closed clockwise polyline that must strictly contain all p (no points exactly on or outside the polyline) returns: c, v where: c is a numpy float array of shape (M+E, 2) being the circumcircle centres of the M triangles and E boundary points from the aoi polygon line; and v is a list of N Voronoi cell lists of clockwise ints, each int being an index into c notes: the aoi polyline forms the outer boundary for the Voronoi polygons for points on the outer edge of the triangulation; all points p must lie strictly within the aoi, which must be convex; the triangulation t, of points p, must also have a convex hull; note that the dt() function can produce a triangulation with slight concavities on the hull, especially for smaller values of its container_size_factor argument """ # this code assumes that the Voronoi polygon for a seed point visits the circumcentres of # all the triangles that make use of the point – currently understood to be always the case # for a Delauney triangulation def __aoi_intervening_nodes(aoi_count, c_count, seg_a, seg_c): nodes = [] seg = seg_a while seg != seg_c: seg = (seg + 1) % aoi_count nodes.append(c_count + seg) return nodes def __shorter_sides_p_i(p3): max_length = -1.0 max_i = None for i in range(3): opp_length = vec.naive_length(p3[i - 1] - p3[i - 2]) if opp_length > max_length: max_length = opp_length max_i = i return max_i def __azi_between(a, c, t): if c < a: c += 360.0 return (a <= t <= c) or (a <= t + 360.0 <= c) def __seg_for_ci(ci): # returns hull segment for a boundary index nonlocal ca_count, cah_count, caho_count, cahon_count, wing_hull_segments if ci < ca_count: return None if ci < cah_count: # hull edge intersection return ci - ca_count if ci < caho_count: # wings oi, wing = divmod(ci - cah_count, 2) return wing_hull_segments[oi, wing] if ci < cahon_count: # virtual centre for hull edge return ci - caho_count # else virtual centre for hull point; arbitrarily pick clockwise segment return ci - cahon_count def __intervening_aoi_indices(aoi_count, aoi_intersect_segments, c_count, ca_count, cah_count, ci_for_p, out_pair_intersect_segments): # build list of intervening aoi boundary point indices and append to list aoi_nodes = [] r = [0] if len(ci_for_p) == 2 else range(len(ci_for_p)) just_done_pair = False for cii in r: cip = ci_for_p[cii] ci = ci_for_p[(cii + 1) % len(ci_for_p)] if cip >= c_count and ci >= c_count and not just_done_pair: # identify aoi segments if cip < cah_count: aoi_seg_a = aoi_intersect_segments[cip - ca_count] else: aoi_seg_a = out_pair_intersect_segments[divmod( cip - cah_count, 2)] if ci < cah_count: aoi_seg_c = aoi_intersect_segments[ci - ca_count] else: aoi_seg_c = out_pair_intersect_segments[divmod( ci - cah_count, 2)] aoi_nodes += __aoi_intervening_nodes(aoi_count, c_count, aoi_seg_a, aoi_seg_c) just_done_pair = True else: just_done_pair = False return aoi_nodes def __ci_non_hull(b_i, c, c_count, ci_for_p, p, p_i): trimmed_ci = [] cii = 0 finish_at = len(ci_for_p) while cii < finish_at: if ci_for_p[cii] < c_count: trimmed_ci.append(ci_for_p[cii]) cii += 1 continue start_cii = cii cii_seg = __seg_for_ci(ci_for_p[cii]) while cii == 0 and ci_for_p[start_cii - 1] >= c_count and __seg_for_ci( ci_for_p[start_cii - 1]) == cii_seg: start_cii -= 1 start_cii = start_cii % len(ci_for_p) end_cii = cii + 1 while end_cii < len(ci_for_p) and ci_for_p[ end_cii] >= c_count and __seg_for_ci( ci_for_p[end_cii]) == cii_seg: end_cii += 1 end_cii -= 1 # unpythonesque: end element included in scan if end_cii == start_cii: trimmed_ci.append(ci_for_p[cii]) cii += 1 continue if end_cii < start_cii: finish_at = start_cii if end_cii == (start_cii + 1) % len(ci_for_p): trimmed_ci.append(ci_for_p[start_cii]) trimmed_ci.append(ci_for_p[end_cii]) cii = end_cii + 1 continue start_azi = vec.azimuth(c[ci_for_p[start_cii]] - p[p_i, :2]) end_azi = vec.azimuth(c[ci_for_p[end_cii]] - p[p_i, :2]) scan_cii = start_cii while True: if scan_cii == start_cii: trimmed_ci.append(ci_for_p[scan_cii]) elif scan_cii == end_cii: trimmed_ci.append(ci_for_p[scan_cii]) break else: # if point is around a hull corner from previous, then include (?) next_cii = (scan_cii + 1) % len(ci_for_p) if b_i is None and __seg_for_ci( ci_for_p[scan_cii]) != __seg_for_ci( ci_for_p[next_cii]): trimmed_ci.append(ci_for_p[scan_cii]) trimmed_ci.append(ci_for_p[next_cii]) scan_cii = next_cii elif b_i is None and __seg_for_ci( trimmed_ci[-1]) != __seg_for_ci( ci_for_p[scan_cii]): trimmed_ci.append(ci_for_p[scan_cii]) else: azi = vec.azimuth(c[ci_for_p[scan_cii]] - p[p_i, :2]) if __azi_between(start_azi, end_azi, azi): trimmed_ci.append(ci_for_p[scan_cii]) if scan_cii == end_cii: break scan_cii = (scan_cii + 1) % len(ci_for_p) cii = end_cii + 1 return trimmed_ci def __closest_to_seed(c, c_count, ci_for_p, hull_node_azi, p, p_i): best_a_i = None best_c_i = None best_a_azi = -181.0 best_c_azi = 181.0 for cii, val in enumerate(ci_for_p): if val < c_count: continue azi = vec.azimuth(c[val] - p[p_i, :2]) - hull_node_azi if azi > 180.0: azi -= 360.0 elif azi < -180.0: azi += 360.0 if 0.0 > azi > best_a_azi: best_a_azi = azi best_a_i = cii elif 0.0 <= azi < best_c_azi: best_c_azi = azi best_c_i = cii assert best_a_i is not None and best_c_i is not None trimmed_ci = [] for cii, val in enumerate(ci_for_p): if val < c_count or cii == best_a_i or cii == best_c_i: trimmed_ci.append(val) return trimmed_ci def __ci_replace(c_count, ca_count, cah_count, caho_count, cahon_count, ci_for_p, p, p_i, t, tc_outwith_aoi): # where circumcirle (or virtual) centre is outwith aoi, replace with a point on aoi boundary # virtual centres related to hull points (not hull edges) can be discarded trimmed_ci = [] for ci in ci_for_p: if ci < c_count: # genuine triangle if ci in tc_outwith_aoi: # replace with one or two wing normal intersection points oi = tc_outwith_aoi.index(ci) wing_i = cah_count + 2 * oi shorter_t_i = __shorter_sides_p_i(p[t[ci]]) if t[ci, shorter_t_i] == p_i: trimmed_ci += [wing_i, wing_i + 1] elif t[ci, shorter_t_i - 1] == p_i: trimmed_ci.append(wing_i) else: trimmed_ci.append(wing_i + 1) else: trimmed_ci.append(ci) elif ci < cahon_count: # extended virtual centre for a hull edge (discard hull point virtual centres) # replace with index for intersection point on aoi boundary trimmed_ci.append(ca_count + ci - caho_count) return trimmed_ci def __veroni_cells(aoi_count, aoi_intersect_segments, b, c, c_count, ca_count, cah_count, caho_count, cahon_count, hull_count, out_pair_intersect_segments, p, t, tc_outwith_aoi): # list of voronoi cells (each a numpy list of node indices into c extended with aoi points etc) v = [] # for each seed point build the voronoi cell for p_i in range(len(p)): # find triangles making use of that point ci_for_p = list(np.where(t == p_i)[0]) # if seed point is on hull boundary, introduce three extended virtual centres b_i = None if p_i in b: b_i = np.where(b == p_i)[0][0] # index into hull coordinates p_b_i = ( b_i - 1 ) % hull_count # predecessor, ie. anti-clockwise boundary point ci_for_p += [ caho_count + p_b_i, cahon_count + b_i, caho_count + b_i ] # find azimuths of vectors from seed point to circumcircle centres (and virtual centres) azi = [ vec.azimuth(centre - p[p_i, :2]) for centre in c[ci_for_p, :2] ] # if this is a hull seed point, make a note of azimuth to virtual centre hull_node_azi = None if b_i is None else azi[-2] # sort triangle indices for seed point into clockwise order of circumcircle (and virtual) centres ci_for_p = [ti for (_, ti) in sorted(zip(azi, ci_for_p))] ci_for_p = __ci_replace(c_count, ca_count, cah_count, caho_count, cahon_count, ci_for_p, p, p_i, t, tc_outwith_aoi) # if this is a hull seed point, classify aoi boundary points into anti-clockwise or clockwise, and find # closest to seed if b_i is not None: ci_for_p = __closest_to_seed(c, c_count, ci_for_p, hull_node_azi, p, p_i) # for sequences on aoi boundary, just keep those between the first and last (?) # elif any([ci < c_count for ci in ci_for_p]): else: ci_for_p = __ci_non_hull(b_i, c, c_count, ci_for_p, p, p_i) # reverse points if needed for pair of aoi points only assert len(ci_for_p) >= 2 if len(ci_for_p) == 2: seg_0 = __seg_for_ci(ci_for_p[0]) seg_1 = __seg_for_ci(ci_for_p[1]) if seg_0 is not None and seg_1 is not None and seg_0 == ( seg_1 + 1) % hull_count: ci_for_p.reverse() ci_for_p += __intervening_aoi_indices(aoi_count, aoi_intersect_segments, c_count, ca_count, cah_count, ci_for_p, out_pair_intersect_segments) # remove circumcircle centres that are outwith area of interest ci_for_p = np.array([ ti for ti in ci_for_p if ti >= c_count or ti not in tc_outwith_aoi ], dtype=int) # find azimuths of vectors from seed point to circumcircle centres and aoi boundary points azi = [ vec.azimuth(centre - p[p_i, :2]) for centre in c[ci_for_p, :2] ] # re-sort triangle indices for seed point into clockwise order of circumcircle centres and boundary points ordered_ci = [ti for (_, ti) in sorted(zip(azi, ci_for_p))] v.append(ordered_ci) return v # log.debug(f'\n\nVoronoi: nt: {len(p)}; nt: {len(t)}; hull: {len(b)}; aoi: {len(aoi.coordinates)}') # todo: allow aoi to be None in which case create an aoi as hull with border assert p.ndim == 2 and p.shape[0] > 2 and p.shape[1] >= 2 assert t.ndim == 2 and t.shape[1] == 3 assert b.ndim == 1 and b.shape[0] > 2 assert len(aoi.coordinates) >= 3 assert aoi.isclosed assert aoi.is_clockwise() # create temporary polyline for hull of triangulation hull = rql.Polyline( aoi.model, set_bool=True, # polyline is closed set_coord=p[b], set_crs=aoi.crs_uuid, title='triangulation hull') hull_count = len(b) # check for concavities in hull if not hull.is_convex(): log.warning( 'Delauney triangulation is not convex; Voronoi diagram construction might fail' ) # compute circumcircle centres c = np.zeros((t.shape[0], 2)) for ti in range(len(t)): c[ti] = ccc(p[t[ti, 0]], p[t[ti, 1]], p[t[ti, 2]]) c_count = len(c) # make list of triangle indices whose circumcircle centres are outwith the area of interest tc_outwith_aoi = [ ti for ti in range(c_count) if not aoi.point_is_inside_xy(c[ti]) ] o_count = len(tc_outwith_aoi) # make space for combined points data needed for all voronoi cell nodes: # 1. circumcircle centres for triangles in delauney triangulation # 2. nodes defining area of interest polygon # 3. intersection of normals to triangulation hull edges with aoi polygon # 4. extra intersections for normals to other two (non-hull) triangle edges, with aoi, # where circumcircle centre is outside the area of interest # 5. extended ccc for hull edge normals # 6. extended ccc for hull points # (5 & 6 only used during construction) c = np.concatenate( (c, aoi.coordinates[:, :2], np.zeros((hull_count, 2), dtype=float), np.zeros((2 * o_count, 2), dtype=float), np.zeros((hull_count, 2), dtype=float), np.zeros((hull_count, 2), dtype=float))) aoi_count = len(aoi.coordinates) ca_count = c_count + aoi_count cah_count = ca_count + hull_count caho_count = cah_count + 2 * o_count cahon_count = caho_count + hull_count assert cahon_count + hull_count == len(c) # compute intersection points between hull edge normals and aoi polyline # also extended virtual centres for hull edges extension_scaling = 1000.0 * np.sum((np.max(aoi.coordinates, axis=0) - np.min(aoi.coordinates, axis=0))[:2]) aoi_intersect_segments = np.empty((hull_count, ), dtype=int) for ei in range(hull_count): # use segment midpoint and normal methods of hull to project out m = hull.segment_midpoint(ei)[:2] # midpoint norm_vec = hull.segment_normal(ei)[:2] n = m + norm_vec # point on normal # use first intersection method of aoi to intersect projected normal from triangulation hull aoi_seg, aoi_x, aoi_y = aoi.first_line_intersection(m[0], m[1], n[0], n[1], half_segment=True) assert aoi_seg is not None # inject intersection points to extension area of c and take note of aoi segment of intersection c[ca_count + ei] = (aoi_x, aoi_y) aoi_intersect_segments[ei] = aoi_seg # inject extended virtual circle centres for hull edges, a long way out c[caho_count + ei] = c[ca_count + ei] + extension_scaling * norm_vec # compute extended virtual centres for hull nodes for ei in range(hull_count): pei = (ei - 1) % hull_count vector = vec.unit_vector( hull.segment_normal(pei)[:2] + hull.segment_normal(ei)[:2]) c[cahon_count + ei] = hull.coordinates[ei, :2] + extension_scaling * vector # where cicrumcircle centres are outwith aoi, compute intersections of normals of wing edges with aoi out_pair_intersect_segments = np.empty((o_count, 2), dtype=int) wing_hull_segments = np.empty((o_count, 2), dtype=int) for oi, ti in enumerate(tc_outwith_aoi): tpi = __shorter_sides_p_i(p[t[ti]]) for wing in range(2): # note: triangle nodes are anticlockwise m = 0.5 * (p[t[ti, tpi - 1]] + p[t[ti, tpi]])[:2] # triangle edge midpoint edge_v = p[t[ti, tpi]] - p[t[ti, tpi - 1]] n = m + np.array( (-edge_v[1], edge_v[0] )) # point on perpendicular bisector of triangle edge o_seg, o_x, o_y = aoi.first_line_intersection(m[0], m[1], n[0], n[1], half_segment=True) c[cah_count + 2 * oi + wing] = (o_x, o_y) out_pair_intersect_segments[oi, wing] = o_seg wing_hull_segments[oi, wing], _, _ = hull.first_line_intersection( m[0], m[1], n[0], n[1], half_segment=True) tpi = (tpi + 1) % 3 v = __veroni_cells(aoi_count, aoi_intersect_segments, b, c, c_count, ca_count, cah_count, caho_count, cahon_count, hull_count, out_pair_intersect_segments, p, t, tc_outwith_aoi) return c[:caho_count], v