weavingspace.topology_elements
1#!/usr/bin/env python 2# coding: utf-8 3 4import shapely.geometry as geom 5import weavingspace.tiling_utils as tiling_utils 6 7class Tile(object): 8 """Class to capture and manipulate essential features of polygons in a tiling. 9 """ 10 ID: int 11 """integer ID number which indexes the Tile in the containing Topology tiles 12 list.""" 13 base_ID: int 14 """ID of corresponding Tile in the base tileable unit""" 15 corners: list["Vertex"] 16 """list of Vertex objects. This includes all corners of the original polygon 17 and any tiling vertices induced by (for example) a the corner of an adjacent 18 tile lying halfway along an edge of the original polygon on which this tile 19 is based. Vertex objects are stored in strictly clockwise sequence.""" 20 edges: list["Edge"] 21 """list of Edge objects that together compose the tile boundary.""" 22 edges_CW: list[bool] 23 """list of Edge direction. Edges are stored only once in a Topology so some 24 edges are in clockwise order and others are in counter-clockwise order. 25 These boolean flags are True if the corresponding Edge is clockwise, False if 26 counter-clockwise.""" 27 label: str 28 """tile_id label from the tileable source""" 29 vertex_labels: list[str] 30 """list of (upper case) letter labels of the tile corners (i.e. all corners, 31 not only tiling vertices).""" 32 edge_labels: list[str] 33 """list of (lower case) letter labels of the tile edges (tiling edges, not 34 tile sides).""" 35 shape: geom.Polygon = None 36 """the tile geometry (which may include some redundant points along sides 37 where neighbouring tiles induce a tiling vertex). So for example a rectangle 38 might have additional points along its sides: 39 40 +---+-------+ 41 | | 2 | 42 | 1 A---B---E---+ 43 | | | 4 | 44 +---C 3 D-------+ 45 | | 46 +---+ 47 48 In the above Tile 1 has additional point A, 2 has B and 3 has C and D induced 49 by the corners of neighbouring tiles.""" 50 centre: geom.Point = None 51 """a point centre for the Tile (determined by weavingspace.tiling_utils. 52 incentre).""" 53 shape_group: int = None 54 """the tile shape group of this tile in its containing Topology.""" 55 transitivity_class: int = None 56 """the tile transitivity class of this tile its containing Topology""" 57 58 def __init__(self, ID:int): 59 """Class constructor. 60 61 Args: 62 ID (int): Tile ID which indexes it in the containing Topology tiles list. 63 """ 64 # self.set_shape(tiling_utils.get_clean_polygon(shape)) 65 self.ID = ID 66 self.corners = [] 67 self.edges = [] 68 self.edges_CW = [] 69 self.vertex_labels = [] 70 self.edge_labels = [] 71 72 def __str__(self) -> str: 73 """Returns string representation of the Tile. 74 75 Returns: 76 str: string including Tile ID, list of corner vertex IDs and list of 77 edge IDs. 78 """ 79 return f"Tile {self.ID} Corners: {self.get_corner_IDs()} Edges: {self.get_edge_IDs()}" 80 81 def __repr__(self) -> str: 82 return str(self) 83 84 def get_corner_IDs(self) -> list[int]: 85 """Convenience method to return list of corner IDs (not Vertex objects). 86 87 Returns: 88 list[int]: list of integer IDs of tile corners. 89 """ 90 return [c.ID for c in self.corners] 91 92 def get_edge_IDs(self) -> list[tuple[int]]: 93 """Convenience method to return list of edge IDs (not Edge objects). 94 95 Returns: 96 list[tuple[int]]: list of 2-element tuples of the start and end Vertex IDs 97 of each edge. 98 """ 99 return [e.ID for e in self.edges] 100 101 def set_shape_from_corners(self): 102 """Sets the shape attribute based on the current list of corners, and sets 103 the associated tile centre. 104 """ 105 self.shape = geom.Polygon([c.point for c in self.corners]) 106 self.centre = tiling_utils.incentre( 107 tiling_utils.get_clean_polygon(self.shape)) 108 109 def set_corners_from_edges(self, update_shape:bool = True): 110 """Sets the corners attribute from the edges attribute. Typically called 111 after modification of topology edges. Optionally the shape attribute is NOT 112 updated, which may save time when multiple changes to the edges of a tile 113 are in process (i.e., only update the shape after all changes are complete). 114 115 Args: 116 update_shape (bool, optional): if True the shape attribute will be 117 updated, otherwise not. Defaults to True. 118 """ 119 self.corners = [] 120 for e, cw in zip(self.edges, self.edges_CW): 121 if cw: # clockwise to extend by all but the first corner 122 self.corners.extend(e.corners[:-1]) 123 else: # counter-clockwise so extend in reverse 124 self.corners.extend(e.corners[1:][::-1]) 125 if update_shape: 126 self.set_shape_from_corners() 127 128 def set_edge_directions(self): 129 """Sets up the edges_CW attribute by inspection of the edges list. 130 It is (frankly!) hard to keep track of the correct sequence of CW/CCW order 131 of edges as new ones are created or old ones merged. This method inspects 132 the 'tail-head' relations between consecutive edges to set these flags 133 correctly. 134 135 The test is simply to check if the 'tail' Vertex ID in each edge appears 136 in the ID tuple of the following edge, i.e. if successive edge 137 IDs are (0, 1) (2, 1) or (0, 1) (1, 2), then edge (0, 1) is in clockwise 138 direction, but if we have (0, 1) (2, 3) then it is not. 139 """ 140 edge_IDs = self.get_edge_IDs() 141 self.edges_CW = [e1[-1] in e2 for e1, e2 in 142 zip(edge_IDs, edge_IDs[1:] + edge_IDs[:1])] 143 144 def insert_vertex_at(self, v:"Vertex", i:int, 145 update_shape:bool = False) -> tuple: 146 """Method to insert the supplied Vertex into tile at index position i, 147 optionally updating the shape attribute. Both corners and edges attributes 148 are updated, and the old edge IDs for removal and the new edge itself are 149 returned to the calling context (the containing Topology) for update of its 150 edges collection. 151 152 This is NOT a generic vertex insertion method: it is only for use during 153 Topology initialisation, and does not guarantee correct maintenance of 154 all tile, edge and vertex relations in the general case---at any rate it 155 has not been tested for this! 156 157 Args: 158 v (Vertex): the Vertex to insert. 159 i (int): index position in current corners after which to insert 160 supplied Vertex. 161 update_shape (bool, optional): if True shape attribute is updated. 162 Defaults to False. 163 164 Returns: 165 tuple: the (tuple) ID of the old edge which should be deleted, and 166 the new Edges arising from insertion of this Vertex. 167 """ 168 self.corners = self.corners[:i] + [v] + self.corners[i:] 169 old_edge = self.edges[i - 1] 170 # store current ID of the affected edge for return to calling context 171 old_edge_ID = old_edge.ID 172 new_edges = old_edge.insert_vertex(v, self.corners[i - 1]) 173 self.edges = self.edges[:(i-1)] + new_edges + self.edges[i:] 174 self.set_edge_directions() 175 if update_shape: 176 self.set_shape_from_corners() 177 return old_edge_ID, new_edges 178 179 def merge_edges_at_vertex(self, v:"Vertex") -> tuple: 180 """Method to merge the edges that meet at the supplied Vertex. It is 181 assumed that only two tiles are impacted this one, and its neighbour across 182 the Edge on which v lies. Both are updated. For this reason the work is 183 delegated to get_updated_edges_from_merge which is run on both affected 184 tiles, but only determines the edges to remove and the new edge to be added 185 once. See that method for details. 186 187 Args: 188 v (Vertex): Vertex at which to merge Edges. This should currently be an 189 end 190 191 Returns: 192 tuple: 2 item list of the edge IDs to be removed and a new Edge object to 193 be added by the calling context (i.e. the containing Topology). 194 """ 195 to_remove, new_edge = self.get_updated_edges_from_merge(v) 196 if len(v.tiles) > 1: 197 v.tiles[1].get_updated_edges_from_merge(v, new_edge) 198 return to_remove, new_edge 199 200 def get_updated_edges_from_merge(self, v:"Vertex", new_edge:"Edge" = None): 201 """Updates the edges and edges_CW attributes based on insertion of 202 the supplied Vertex. If new_edge is supplied then the neighbour tile at 203 v has already created the needed new Edge and this Edge is the one that 204 will be 'slotted in' at the appropriate spot in the edges list. 205 206 The edges_CW is also updated to maintain correct directions of the edges. 207 The corners attribute is unaffected by these changes. 208 209 Args: 210 v (Vertex): existing Vertex at which to carry out the merge. 211 new_edge (Edge, optional): if another Tile has already carried out this 212 merge this should be the resulting new Edge for insertion into this 213 Tile. Defaults to None (when the new Edge will be constructed). 214 215 Returns: 216 Union[None, tuple]: either None (if a new edge was supplied) or a tuple 217 of the two edge IDs to be removed and the new edge added for return to 218 the calling context (i.e. the containing Topology). 219 """ 220 # get the two edge list index positions in which Vertex v is found 221 i, j = self.get_edge_IDs_including_vertex(v) 222 if new_edge is None: # then we must make a new one 223 # also record existing edge IDs to be removed 224 to_remove = [self.edges[i].ID, self.edges[j].ID] 225 new_edge = self.get_merged_edge(i, j) 226 return_edge_updates = True 227 else: 228 return_edge_updates = False 229 if abs(i - j) != 1: 230 # edge indices 'wrap' around from end of edge list to start so drop 231 # first and last current edges and stick new one on at the end 232 self.edges = self.edges[1:-1] + [new_edge] 233 else: 234 # insert new edge into list in place of the two old ones 235 self.edges = self.edges[:i] + [new_edge] + self.edges[j+1:] 236 # update the edge directions 237 self.set_edge_directions() 238 if return_edge_updates: 239 return to_remove, new_edge 240 else: 241 return None 242 243 def get_edge_IDs_including_vertex(self, v:"Vertex") -> tuple[int]: 244 """Gets the (two) index positions of the edges that include supplied Vertex. 245 246 Args: 247 v (Vertex): Vertex of interest. 248 249 Returns: 250 tuple[int]: two index positions of Edges in edges list that contain v. 251 """ 252 return (i for i, e in enumerate(self.edges) if v.ID in e.ID) 253 254 def get_merged_edge(self, i:int, j:int) -> "Edge": 255 """Returns the new edge resulting from merging the two existing edges at 256 index positions i and j in the edges list. For example, if the current list 257 of edge IDs was 258 259 (0 1 2) (4 2) (4 5) (5 0) 260 261 and the merge Vertex was #2, the resulting new edge is constructed from 262 vertices (0 1 2 4). 263 264 Returns: 265 Edge: the requested new Edge. 266 """ 267 # if i and j are not consecutive, then j is predecessor edge 268 if abs(i - j) != 1: 269 i, j = j, i 270 # get edges and their directions 271 ei, ej = self.edges[i], self.edges[j] 272 CWi, CWj = self.edges_CW[i], self.edges_CW[j] 273 # DON'T MESS WITH THIS!!! 274 # for predecessors (the head) we want everything including the Vertex 275 # where the merge is occurring; for successors (the tail) we want all but 276 # the first Vertex (which is the one where the merge is occurring). In both 277 # cases contingent on whether existing Edges are CW or CCW we may need to 278 # flip the Vertex sequence to ensure that the merge Vertex is in the middle 279 # of the new edge that will be created 280 head = ei.corners if CWi else ei.corners[::-1] 281 tail = ej.corners[1:] if CWj else ej.corners[::-1][1:] 282 v_sequence = (head if CWi else head[::-1]) + (tail if CWj else tail[::-1]) 283 return Edge(v_sequence) 284 285 def offset_corners(self, offset:int): 286 """Shifts shape, corners, edges, and edges_CW by an offset amount. This is 287 used to align tiles that are similar, which is required for correct 288 transfer of 'base' tile labelling on to 'radius 1' tiles during Topology 289 construction. 290 291 Args: 292 offset int: the number of positions to shift the lists. 293 """ 294 if not offset is None or offset == 0: 295 self.shape = tiling_utils.offset_polygon_corners(self.shape, offset) 296 self.corners = self.corners[offset:] + self.corners[:offset] 297 self.edges = self.edges[offset:] + self.edges[:offset] 298 self.edges_CW = self.edges_CW[offset:] + self.edges_CW[:offset] 299 300 def get_edge_label(self, e:"Edge") -> str: 301 """Returns edge label of specified edge. 302 303 Args: 304 e (Edge): Edge whose label is required. 305 306 Returns: 307 str: requested edge label. 308 """ 309 return self.edge_labels[self.get_edge_IDs().index(e.ID)] 310 311 def get_corner_label(self, v:"Vertex") -> str: 312 """Returns corner label of specified corner. 313 314 Args: 315 v (Vertex): corner whose label is required. 316 317 Returns: 318 str: requested corner label. 319 """ 320 return self.edge_labels[self.get_corner_IDs().index(v.ID)] 321 322 def get_vertex_label_positions(self) -> list[geom.Point]: 323 """Returns a viable location at which to position corner labels inside 324 tile shape. The method is convoluted because a negative buffer may remove 325 colinear corners resulting in fewer positions than we have corners in the 326 tile shape! 327 328 Returns: 329 list[geom.Point]: list of locations. 330 """ 331 d = (self.shape.area ** 0.5) / 8 332 c = self.centre 333 corners = [c.point for c in self.corners] 334 return [geom.LineString([p, c]).line_interpolate_point(d) for p in corners] 335 336 def get_edge_label_positions(self) -> list[geom.Point]: 337 """Returns a reasonable location at which to position edge labels inside 338 tile shape. 339 340 Returns: 341 list[geom.Point]: list of locations 342 """ 343 d = (self.shape.area ** 0.5) / 8 344 c = self.centre 345 # note direction is important as edge might not be a simple line segment 346 sides = [e.get_geometry(CW) for e, CW in zip(self.edges, self.edges_CW)] 347 return [geom.LineString([s.centroid, c]).line_interpolate_point(d) 348 for s in sides] 349 350 def angle_at(self, v:"Vertex") -> float: 351 """Returns interior angle at the specified corner (in degrees). 352 353 Args: 354 v (Vertex): corner where angle is requested. 355 356 Returns: 357 float: angle at corner v in degrees. 358 """ 359 i = self.corners.index(v) 360 n = len(self.corners) 361 return tiling_utils.get_inner_angle(self.corners[i-1].point, 362 self.corners[i].point, 363 self.corners[(i + 1) % n].point) 364 365 366class Vertex: 367 """Class to store attributes of a vertex in a tiling.""" 368 point: geom.Point 369 """point (geom.Point): point location of the vertex.""" 370 ID: int 371 """integer (mostly but not necessarily in sequence) of vertex keyed into the 372 points dictionary of the containing Topology.""" 373 tiles: list["Tile"] 374 """list of Tiles incident on this vertex.""" 375 neighbours: list[int] 376 """list of the immediately adjacent other corner IDs. Only required to 377 determine if a point is a tiling vertex (when it will have) three or more 378 neighbours, so only IDs are stored.""" 379 base_ID: int = 1_000_000 380 """ID of corresponding Vertex in the tileable base_unit""" 381 transitivity_class: int = None 382 """transitivity class of the vertex under symmetries of the tiling""" 383 label: str = "" 384 """the (upper case letter) label of the vertex under the symmetries of the 385 tiling.""" 386 is_tiling_vertex: bool = True 387 """is_tiling_vertex (bool): True if this is a tiling vertex, rather than a 388 tile corner. E.g., A below is a corner, not a tiling vertex. B is a tiling 389 vertex: 390 391 +-------+ 392 | 1 | 393 | A---B---+ 394 | | 2 | 395 +---C +---+ 396 | | 397 +---+""" 398 399 def __init__(self, point:geom.Point, ID:int): 400 """Class constructor. 401 402 Args: 403 point (geom.Point): point location of the vertex. 404 ID (int): a unique integer ID (which will be its key in the containing 405 Topology points dictionary). 406 """ 407 self.point = point 408 self.ID = ID 409 self.base_ID = self.ID 410 self.tiles = [] 411 self.neighbours = [] 412 413 def __str__(self) -> str: 414 """Returns string representation of Vertex. 415 416 Returns: 417 str: string including ID, point and list of incident Tiles. 418 """ 419 return f"Vertex {self.ID} at {self.point} Tiles: {self.get_tile_IDs()}" 420 421 def __repr__(self) -> str: 422 return str(self) 423 424 def get_tile_IDs(self) -> list[int]: 425 """Convenience method to return list of Tile IDs (not the Tiles themselves). 426 427 Returns: 428 list[int]: list of Tile IDs 429 """ 430 return [t.ID for t in self.tiles] 431 432 def add_tile(self, tile:Tile): 433 """Adds supplied Tile to the tiles list if it is not already present. 434 435 Args: 436 tile (Tile): Tile to add. 437 """ 438 if not tile in self.tiles: 439 self.tiles.append(tile) 440 441 def add_neighbour(self, vertex_id:int): 442 """Adds supplied ID to the neighbours list if it is not already present 443 444 Args: 445 vertex_id (int): ID to add to the neighbours list. 446 """ 447 if not vertex_id in self.neighbours: 448 self.neighbours.append(vertex_id) 449 450 def clockwise_order_incident_tiles(self): 451 """Reorders the tiles list clockwise (this is for dual tiling construction) 452 """ 453 cw_order = tiling_utils.order_of_pts_cw_around_centre( 454 [t.centre for t in self.tiles], self.point) 455 self.tiles = [self.tiles[i] for i in cw_order] 456 457 def is_interior(self) -> bool: 458 """Tests if vertex is completely enclosed by its incident Tiles. Based on 459 summing the interior angles of the incident tiles at this vertex. 460 461 Returns: 462 bool: True if vertex is completely enclosed by incident Tiles. 463 """ 464 return abs(360 - sum([t.angle_at(self) for t in self.tiles])) \ 465 < tiling_utils.RESOLUTION 466 467 468class Edge: 469 """Class to represent edges in a tiling (not tile sides) per the definitions 470 in Grünbaum and Shephard. 471 """ 472 ID: tuple[int] 473 """IDs of the vertices at ends of the edge. Used as key in the containing 474 Topology's edges dictionary.""" 475 vertices: list["Vertex"] 476 """two item list of the end vertices.""" 477 corners: list["Vertex"] 478 """list of all the vertices in the edge (including its end vertices). In a 479 'normal' edge to edge tiling corners and vertices will be identical.""" 480 right_tile: "Tile" = None 481 """the tile to the right of the edge traversed from its first to its last 482 vertex. Given clockwise winding default, all edges will have a right_tile.""" 483 left_tile: "Tile" = None 484 """the tile to the left of the edge traversed from its first to its last 485 vertex. Exterior edges of the tiles in a Topology will not have a left_tile. 486 """ 487 base_ID: tuple[int] = (1_000_000, 1_000_000) 488 """ID of corresponding edge in the base tileable""" 489 transitivity_class: int = None 490 """transitivity class of the edge under symmetries of the tiling""" 491 label: str = "" 492 """the (lower case letter) label of the edge under the symmetries of the 493 tiling.""" 494 495 def __init__(self, corners:list[Vertex]): 496 """Class constructor. Initialises the corners and vertices lists and sets ID 497 to (vertices[0].ID, vertices[1].ID). The vertices list is all the corners 498 with is_tiling_vertex property True -- Note that during initialisation the 499 default of this property is True until after the relations between tiles and 500 vertices have been determined. 501 502 Args: 503 corners (list[Vertex]): list of all corners along the edge. 504 """ 505 self.corners = corners 506 self.vertices = [v for v in self.corners if v.is_tiling_vertex] 507 self.ID = tuple(v.ID for v in self.vertices) 508 509 def __str__(self) -> str: 510 """Returns a string representation of the Edge. 511 512 Returns: 513 str: include ID and a list of corner vertex IDs. 514 """ 515 return f"Edge {self.ID} Corners: {[c.ID for c in self.corners]}" 516 517 def __repr__(self) -> str: 518 return str(self) 519 520 def get_corner_IDs(self) -> list[int]: 521 """Convenience method to get the IDs of edge corners. 522 523 Returns: 524 list[int]: IDs of all corners. 525 """ 526 return [c.ID for c in self.corners] 527 528 def get_vertex_IDs(self) -> list[int]: 529 """Convenience method to get IDs of edge vertices. 530 531 Returns: 532 list[int]: list of IDs of the vertices. 533 """ 534 return [v.ID for v in self.vertices] 535 536 def insert_vertex(self, v:"Vertex", predecessor:"Vertex") -> list["Edge"]: 537 """Inserts a vertex along this edge after the specified predecessor 538 Vertex and returns this edge modified and a new edge. 539 540 If the initial edge was (say) (0 1 2 5) and the predecessor was set to 1 541 the returned edges would be (0 1 v) and (v 2 5). 542 """ 543 i = self.corners.index(predecessor) 544 new_edge = Edge([v] + self.corners[(i+1):]) 545 if not self.right_tile is None: 546 new_edge.right_tile = self.right_tile 547 if not self.left_tile is None: 548 new_edge.left_tile = self.left_tile 549 self.corners = self.corners[:(i+1)] + [v] 550 self.vertices = [self.vertices[0], v] 551 self.ID = tuple(v.ID for v in self.vertices) 552 return [self, new_edge] 553 554 def get_geometry(self, forward = True) -> geom.LineString: 555 """Returns a geom.LineString representing the geometry (including all 556 corners) of this Edge, optionally starting at either end. 557 558 Args: 559 forward (bool, optional): if True the returned LineString starts at 560 corners[0], else at corners[-1]. Defaults to True. 561 562 Returns: 563 geom.LineString: the required LineString. 564 """ 565 if forward: 566 return geom.LineString([v.point for v in self.corners]) 567 else: 568 return geom.LineString([v.point for v in self.corners[::-1]]) 569 570 def get_topology(self, forward = True) -> geom.LineString: 571 """Returns a LineString connecting the first and last corners (i.e. the 572 vertices) of this tilin edge, optionally starting from either end. 573 574 Args: 575 forward (bool, optional): if True LineString starts at vertices[0], 576 else at vertices[1]. Defaults to True. 577 578 Returns: 579 geom.LineString: the required LineString. 580 """ 581 if forward: 582 return geom.LineString([v.point for v in self.vertices]) 583 else: 584 return geom.LineString([v.point for v in self.vertices[::-1]])
8class Tile(object): 9 """Class to capture and manipulate essential features of polygons in a tiling. 10 """ 11 ID: int 12 """integer ID number which indexes the Tile in the containing Topology tiles 13 list.""" 14 base_ID: int 15 """ID of corresponding Tile in the base tileable unit""" 16 corners: list["Vertex"] 17 """list of Vertex objects. This includes all corners of the original polygon 18 and any tiling vertices induced by (for example) a the corner of an adjacent 19 tile lying halfway along an edge of the original polygon on which this tile 20 is based. Vertex objects are stored in strictly clockwise sequence.""" 21 edges: list["Edge"] 22 """list of Edge objects that together compose the tile boundary.""" 23 edges_CW: list[bool] 24 """list of Edge direction. Edges are stored only once in a Topology so some 25 edges are in clockwise order and others are in counter-clockwise order. 26 These boolean flags are True if the corresponding Edge is clockwise, False if 27 counter-clockwise.""" 28 label: str 29 """tile_id label from the tileable source""" 30 vertex_labels: list[str] 31 """list of (upper case) letter labels of the tile corners (i.e. all corners, 32 not only tiling vertices).""" 33 edge_labels: list[str] 34 """list of (lower case) letter labels of the tile edges (tiling edges, not 35 tile sides).""" 36 shape: geom.Polygon = None 37 """the tile geometry (which may include some redundant points along sides 38 where neighbouring tiles induce a tiling vertex). So for example a rectangle 39 might have additional points along its sides: 40 41 +---+-------+ 42 | | 2 | 43 | 1 A---B---E---+ 44 | | | 4 | 45 +---C 3 D-------+ 46 | | 47 +---+ 48 49 In the above Tile 1 has additional point A, 2 has B and 3 has C and D induced 50 by the corners of neighbouring tiles.""" 51 centre: geom.Point = None 52 """a point centre for the Tile (determined by weavingspace.tiling_utils. 53 incentre).""" 54 shape_group: int = None 55 """the tile shape group of this tile in its containing Topology.""" 56 transitivity_class: int = None 57 """the tile transitivity class of this tile its containing Topology""" 58 59 def __init__(self, ID:int): 60 """Class constructor. 61 62 Args: 63 ID (int): Tile ID which indexes it in the containing Topology tiles list. 64 """ 65 # self.set_shape(tiling_utils.get_clean_polygon(shape)) 66 self.ID = ID 67 self.corners = [] 68 self.edges = [] 69 self.edges_CW = [] 70 self.vertex_labels = [] 71 self.edge_labels = [] 72 73 def __str__(self) -> str: 74 """Returns string representation of the Tile. 75 76 Returns: 77 str: string including Tile ID, list of corner vertex IDs and list of 78 edge IDs. 79 """ 80 return f"Tile {self.ID} Corners: {self.get_corner_IDs()} Edges: {self.get_edge_IDs()}" 81 82 def __repr__(self) -> str: 83 return str(self) 84 85 def get_corner_IDs(self) -> list[int]: 86 """Convenience method to return list of corner IDs (not Vertex objects). 87 88 Returns: 89 list[int]: list of integer IDs of tile corners. 90 """ 91 return [c.ID for c in self.corners] 92 93 def get_edge_IDs(self) -> list[tuple[int]]: 94 """Convenience method to return list of edge IDs (not Edge objects). 95 96 Returns: 97 list[tuple[int]]: list of 2-element tuples of the start and end Vertex IDs 98 of each edge. 99 """ 100 return [e.ID for e in self.edges] 101 102 def set_shape_from_corners(self): 103 """Sets the shape attribute based on the current list of corners, and sets 104 the associated tile centre. 105 """ 106 self.shape = geom.Polygon([c.point for c in self.corners]) 107 self.centre = tiling_utils.incentre( 108 tiling_utils.get_clean_polygon(self.shape)) 109 110 def set_corners_from_edges(self, update_shape:bool = True): 111 """Sets the corners attribute from the edges attribute. Typically called 112 after modification of topology edges. Optionally the shape attribute is NOT 113 updated, which may save time when multiple changes to the edges of a tile 114 are in process (i.e., only update the shape after all changes are complete). 115 116 Args: 117 update_shape (bool, optional): if True the shape attribute will be 118 updated, otherwise not. Defaults to True. 119 """ 120 self.corners = [] 121 for e, cw in zip(self.edges, self.edges_CW): 122 if cw: # clockwise to extend by all but the first corner 123 self.corners.extend(e.corners[:-1]) 124 else: # counter-clockwise so extend in reverse 125 self.corners.extend(e.corners[1:][::-1]) 126 if update_shape: 127 self.set_shape_from_corners() 128 129 def set_edge_directions(self): 130 """Sets up the edges_CW attribute by inspection of the edges list. 131 It is (frankly!) hard to keep track of the correct sequence of CW/CCW order 132 of edges as new ones are created or old ones merged. This method inspects 133 the 'tail-head' relations between consecutive edges to set these flags 134 correctly. 135 136 The test is simply to check if the 'tail' Vertex ID in each edge appears 137 in the ID tuple of the following edge, i.e. if successive edge 138 IDs are (0, 1) (2, 1) or (0, 1) (1, 2), then edge (0, 1) is in clockwise 139 direction, but if we have (0, 1) (2, 3) then it is not. 140 """ 141 edge_IDs = self.get_edge_IDs() 142 self.edges_CW = [e1[-1] in e2 for e1, e2 in 143 zip(edge_IDs, edge_IDs[1:] + edge_IDs[:1])] 144 145 def insert_vertex_at(self, v:"Vertex", i:int, 146 update_shape:bool = False) -> tuple: 147 """Method to insert the supplied Vertex into tile at index position i, 148 optionally updating the shape attribute. Both corners and edges attributes 149 are updated, and the old edge IDs for removal and the new edge itself are 150 returned to the calling context (the containing Topology) for update of its 151 edges collection. 152 153 This is NOT a generic vertex insertion method: it is only for use during 154 Topology initialisation, and does not guarantee correct maintenance of 155 all tile, edge and vertex relations in the general case---at any rate it 156 has not been tested for this! 157 158 Args: 159 v (Vertex): the Vertex to insert. 160 i (int): index position in current corners after which to insert 161 supplied Vertex. 162 update_shape (bool, optional): if True shape attribute is updated. 163 Defaults to False. 164 165 Returns: 166 tuple: the (tuple) ID of the old edge which should be deleted, and 167 the new Edges arising from insertion of this Vertex. 168 """ 169 self.corners = self.corners[:i] + [v] + self.corners[i:] 170 old_edge = self.edges[i - 1] 171 # store current ID of the affected edge for return to calling context 172 old_edge_ID = old_edge.ID 173 new_edges = old_edge.insert_vertex(v, self.corners[i - 1]) 174 self.edges = self.edges[:(i-1)] + new_edges + self.edges[i:] 175 self.set_edge_directions() 176 if update_shape: 177 self.set_shape_from_corners() 178 return old_edge_ID, new_edges 179 180 def merge_edges_at_vertex(self, v:"Vertex") -> tuple: 181 """Method to merge the edges that meet at the supplied Vertex. It is 182 assumed that only two tiles are impacted this one, and its neighbour across 183 the Edge on which v lies. Both are updated. For this reason the work is 184 delegated to get_updated_edges_from_merge which is run on both affected 185 tiles, but only determines the edges to remove and the new edge to be added 186 once. See that method for details. 187 188 Args: 189 v (Vertex): Vertex at which to merge Edges. This should currently be an 190 end 191 192 Returns: 193 tuple: 2 item list of the edge IDs to be removed and a new Edge object to 194 be added by the calling context (i.e. the containing Topology). 195 """ 196 to_remove, new_edge = self.get_updated_edges_from_merge(v) 197 if len(v.tiles) > 1: 198 v.tiles[1].get_updated_edges_from_merge(v, new_edge) 199 return to_remove, new_edge 200 201 def get_updated_edges_from_merge(self, v:"Vertex", new_edge:"Edge" = None): 202 """Updates the edges and edges_CW attributes based on insertion of 203 the supplied Vertex. If new_edge is supplied then the neighbour tile at 204 v has already created the needed new Edge and this Edge is the one that 205 will be 'slotted in' at the appropriate spot in the edges list. 206 207 The edges_CW is also updated to maintain correct directions of the edges. 208 The corners attribute is unaffected by these changes. 209 210 Args: 211 v (Vertex): existing Vertex at which to carry out the merge. 212 new_edge (Edge, optional): if another Tile has already carried out this 213 merge this should be the resulting new Edge for insertion into this 214 Tile. Defaults to None (when the new Edge will be constructed). 215 216 Returns: 217 Union[None, tuple]: either None (if a new edge was supplied) or a tuple 218 of the two edge IDs to be removed and the new edge added for return to 219 the calling context (i.e. the containing Topology). 220 """ 221 # get the two edge list index positions in which Vertex v is found 222 i, j = self.get_edge_IDs_including_vertex(v) 223 if new_edge is None: # then we must make a new one 224 # also record existing edge IDs to be removed 225 to_remove = [self.edges[i].ID, self.edges[j].ID] 226 new_edge = self.get_merged_edge(i, j) 227 return_edge_updates = True 228 else: 229 return_edge_updates = False 230 if abs(i - j) != 1: 231 # edge indices 'wrap' around from end of edge list to start so drop 232 # first and last current edges and stick new one on at the end 233 self.edges = self.edges[1:-1] + [new_edge] 234 else: 235 # insert new edge into list in place of the two old ones 236 self.edges = self.edges[:i] + [new_edge] + self.edges[j+1:] 237 # update the edge directions 238 self.set_edge_directions() 239 if return_edge_updates: 240 return to_remove, new_edge 241 else: 242 return None 243 244 def get_edge_IDs_including_vertex(self, v:"Vertex") -> tuple[int]: 245 """Gets the (two) index positions of the edges that include supplied Vertex. 246 247 Args: 248 v (Vertex): Vertex of interest. 249 250 Returns: 251 tuple[int]: two index positions of Edges in edges list that contain v. 252 """ 253 return (i for i, e in enumerate(self.edges) if v.ID in e.ID) 254 255 def get_merged_edge(self, i:int, j:int) -> "Edge": 256 """Returns the new edge resulting from merging the two existing edges at 257 index positions i and j in the edges list. For example, if the current list 258 of edge IDs was 259 260 (0 1 2) (4 2) (4 5) (5 0) 261 262 and the merge Vertex was #2, the resulting new edge is constructed from 263 vertices (0 1 2 4). 264 265 Returns: 266 Edge: the requested new Edge. 267 """ 268 # if i and j are not consecutive, then j is predecessor edge 269 if abs(i - j) != 1: 270 i, j = j, i 271 # get edges and their directions 272 ei, ej = self.edges[i], self.edges[j] 273 CWi, CWj = self.edges_CW[i], self.edges_CW[j] 274 # DON'T MESS WITH THIS!!! 275 # for predecessors (the head) we want everything including the Vertex 276 # where the merge is occurring; for successors (the tail) we want all but 277 # the first Vertex (which is the one where the merge is occurring). In both 278 # cases contingent on whether existing Edges are CW or CCW we may need to 279 # flip the Vertex sequence to ensure that the merge Vertex is in the middle 280 # of the new edge that will be created 281 head = ei.corners if CWi else ei.corners[::-1] 282 tail = ej.corners[1:] if CWj else ej.corners[::-1][1:] 283 v_sequence = (head if CWi else head[::-1]) + (tail if CWj else tail[::-1]) 284 return Edge(v_sequence) 285 286 def offset_corners(self, offset:int): 287 """Shifts shape, corners, edges, and edges_CW by an offset amount. This is 288 used to align tiles that are similar, which is required for correct 289 transfer of 'base' tile labelling on to 'radius 1' tiles during Topology 290 construction. 291 292 Args: 293 offset int: the number of positions to shift the lists. 294 """ 295 if not offset is None or offset == 0: 296 self.shape = tiling_utils.offset_polygon_corners(self.shape, offset) 297 self.corners = self.corners[offset:] + self.corners[:offset] 298 self.edges = self.edges[offset:] + self.edges[:offset] 299 self.edges_CW = self.edges_CW[offset:] + self.edges_CW[:offset] 300 301 def get_edge_label(self, e:"Edge") -> str: 302 """Returns edge label of specified edge. 303 304 Args: 305 e (Edge): Edge whose label is required. 306 307 Returns: 308 str: requested edge label. 309 """ 310 return self.edge_labels[self.get_edge_IDs().index(e.ID)] 311 312 def get_corner_label(self, v:"Vertex") -> str: 313 """Returns corner label of specified corner. 314 315 Args: 316 v (Vertex): corner whose label is required. 317 318 Returns: 319 str: requested corner label. 320 """ 321 return self.edge_labels[self.get_corner_IDs().index(v.ID)] 322 323 def get_vertex_label_positions(self) -> list[geom.Point]: 324 """Returns a viable location at which to position corner labels inside 325 tile shape. The method is convoluted because a negative buffer may remove 326 colinear corners resulting in fewer positions than we have corners in the 327 tile shape! 328 329 Returns: 330 list[geom.Point]: list of locations. 331 """ 332 d = (self.shape.area ** 0.5) / 8 333 c = self.centre 334 corners = [c.point for c in self.corners] 335 return [geom.LineString([p, c]).line_interpolate_point(d) for p in corners] 336 337 def get_edge_label_positions(self) -> list[geom.Point]: 338 """Returns a reasonable location at which to position edge labels inside 339 tile shape. 340 341 Returns: 342 list[geom.Point]: list of locations 343 """ 344 d = (self.shape.area ** 0.5) / 8 345 c = self.centre 346 # note direction is important as edge might not be a simple line segment 347 sides = [e.get_geometry(CW) for e, CW in zip(self.edges, self.edges_CW)] 348 return [geom.LineString([s.centroid, c]).line_interpolate_point(d) 349 for s in sides] 350 351 def angle_at(self, v:"Vertex") -> float: 352 """Returns interior angle at the specified corner (in degrees). 353 354 Args: 355 v (Vertex): corner where angle is requested. 356 357 Returns: 358 float: angle at corner v in degrees. 359 """ 360 i = self.corners.index(v) 361 n = len(self.corners) 362 return tiling_utils.get_inner_angle(self.corners[i-1].point, 363 self.corners[i].point, 364 self.corners[(i + 1) % n].point)
Class to capture and manipulate essential features of polygons in a tiling.
59 def __init__(self, ID:int): 60 """Class constructor. 61 62 Args: 63 ID (int): Tile ID which indexes it in the containing Topology tiles list. 64 """ 65 # self.set_shape(tiling_utils.get_clean_polygon(shape)) 66 self.ID = ID 67 self.corners = [] 68 self.edges = [] 69 self.edges_CW = [] 70 self.vertex_labels = [] 71 self.edge_labels = []
Class constructor.
Args: ID (int): Tile ID which indexes it in the containing Topology tiles list.
list of Vertex objects. This includes all corners of the original polygon and any tiling vertices induced by (for example) a the corner of an adjacent tile lying halfway along an edge of the original polygon on which this tile is based. Vertex objects are stored in strictly clockwise sequence.
list of Edge direction. Edges are stored only once in a Topology so some edges are in clockwise order and others are in counter-clockwise order. These boolean flags are True if the corresponding Edge is clockwise, False if counter-clockwise.
list of (upper case) letter labels of the tile corners (i.e. all corners, not only tiling vertices).
list of (lower case) letter labels of the tile edges (tiling edges, not tile sides).
the tile geometry (which may include some redundant points along sides where neighbouring tiles induce a tiling vertex). So for example a rectangle might have additional points along its sides:
+---+-------+
| | 2 |
| 1 A---B---E---+
| | | 4 |
+---C 3 D-------+
| |
+---+
In the above Tile 1 has additional point A, 2 has B and 3 has C and D induced by the corners of neighbouring tiles.
a point centre for the Tile (determined by weavingspace.tiling_utils. incentre).
85 def get_corner_IDs(self) -> list[int]: 86 """Convenience method to return list of corner IDs (not Vertex objects). 87 88 Returns: 89 list[int]: list of integer IDs of tile corners. 90 """ 91 return [c.ID for c in self.corners]
Convenience method to return list of corner IDs (not Vertex objects).
Returns: list[int]: list of integer IDs of tile corners.
93 def get_edge_IDs(self) -> list[tuple[int]]: 94 """Convenience method to return list of edge IDs (not Edge objects). 95 96 Returns: 97 list[tuple[int]]: list of 2-element tuples of the start and end Vertex IDs 98 of each edge. 99 """ 100 return [e.ID for e in self.edges]
Convenience method to return list of edge IDs (not Edge objects).
Returns: list[tuple[int]]: list of 2-element tuples of the start and end Vertex IDs of each edge.
102 def set_shape_from_corners(self): 103 """Sets the shape attribute based on the current list of corners, and sets 104 the associated tile centre. 105 """ 106 self.shape = geom.Polygon([c.point for c in self.corners]) 107 self.centre = tiling_utils.incentre( 108 tiling_utils.get_clean_polygon(self.shape))
Sets the shape attribute based on the current list of corners, and sets the associated tile centre.
110 def set_corners_from_edges(self, update_shape:bool = True): 111 """Sets the corners attribute from the edges attribute. Typically called 112 after modification of topology edges. Optionally the shape attribute is NOT 113 updated, which may save time when multiple changes to the edges of a tile 114 are in process (i.e., only update the shape after all changes are complete). 115 116 Args: 117 update_shape (bool, optional): if True the shape attribute will be 118 updated, otherwise not. Defaults to True. 119 """ 120 self.corners = [] 121 for e, cw in zip(self.edges, self.edges_CW): 122 if cw: # clockwise to extend by all but the first corner 123 self.corners.extend(e.corners[:-1]) 124 else: # counter-clockwise so extend in reverse 125 self.corners.extend(e.corners[1:][::-1]) 126 if update_shape: 127 self.set_shape_from_corners()
Sets the corners attribute from the edges attribute. Typically called after modification of topology edges. Optionally the shape attribute is NOT updated, which may save time when multiple changes to the edges of a tile are in process (i.e., only update the shape after all changes are complete).
Args: update_shape (bool, optional): if True the shape attribute will be updated, otherwise not. Defaults to True.
129 def set_edge_directions(self): 130 """Sets up the edges_CW attribute by inspection of the edges list. 131 It is (frankly!) hard to keep track of the correct sequence of CW/CCW order 132 of edges as new ones are created or old ones merged. This method inspects 133 the 'tail-head' relations between consecutive edges to set these flags 134 correctly. 135 136 The test is simply to check if the 'tail' Vertex ID in each edge appears 137 in the ID tuple of the following edge, i.e. if successive edge 138 IDs are (0, 1) (2, 1) or (0, 1) (1, 2), then edge (0, 1) is in clockwise 139 direction, but if we have (0, 1) (2, 3) then it is not. 140 """ 141 edge_IDs = self.get_edge_IDs() 142 self.edges_CW = [e1[-1] in e2 for e1, e2 in 143 zip(edge_IDs, edge_IDs[1:] + edge_IDs[:1])]
Sets up the edges_CW attribute by inspection of the edges list. It is (frankly!) hard to keep track of the correct sequence of CW/CCW order of edges as new ones are created or old ones merged. This method inspects the 'tail-head' relations between consecutive edges to set these flags correctly.
The test is simply to check if the 'tail' Vertex ID in each edge appears in the ID tuple of the following edge, i.e. if successive edge IDs are (0, 1) (2, 1) or (0, 1) (1, 2), then edge (0, 1) is in clockwise direction, but if we have (0, 1) (2, 3) then it is not.
145 def insert_vertex_at(self, v:"Vertex", i:int, 146 update_shape:bool = False) -> tuple: 147 """Method to insert the supplied Vertex into tile at index position i, 148 optionally updating the shape attribute. Both corners and edges attributes 149 are updated, and the old edge IDs for removal and the new edge itself are 150 returned to the calling context (the containing Topology) for update of its 151 edges collection. 152 153 This is NOT a generic vertex insertion method: it is only for use during 154 Topology initialisation, and does not guarantee correct maintenance of 155 all tile, edge and vertex relations in the general case---at any rate it 156 has not been tested for this! 157 158 Args: 159 v (Vertex): the Vertex to insert. 160 i (int): index position in current corners after which to insert 161 supplied Vertex. 162 update_shape (bool, optional): if True shape attribute is updated. 163 Defaults to False. 164 165 Returns: 166 tuple: the (tuple) ID of the old edge which should be deleted, and 167 the new Edges arising from insertion of this Vertex. 168 """ 169 self.corners = self.corners[:i] + [v] + self.corners[i:] 170 old_edge = self.edges[i - 1] 171 # store current ID of the affected edge for return to calling context 172 old_edge_ID = old_edge.ID 173 new_edges = old_edge.insert_vertex(v, self.corners[i - 1]) 174 self.edges = self.edges[:(i-1)] + new_edges + self.edges[i:] 175 self.set_edge_directions() 176 if update_shape: 177 self.set_shape_from_corners() 178 return old_edge_ID, new_edges
Method to insert the supplied Vertex into tile at index position i, optionally updating the shape attribute. Both corners and edges attributes are updated, and the old edge IDs for removal and the new edge itself are returned to the calling context (the containing Topology) for update of its edges collection.
This is NOT a generic vertex insertion method: it is only for use during Topology initialisation, and does not guarantee correct maintenance of all tile, edge and vertex relations in the general case---at any rate it has not been tested for this!
Args: v (Vertex): the Vertex to insert. i (int): index position in current corners after which to insert supplied Vertex. update_shape (bool, optional): if True shape attribute is updated. Defaults to False.
Returns: tuple: the (tuple) ID of the old edge which should be deleted, and the new Edges arising from insertion of this Vertex.
180 def merge_edges_at_vertex(self, v:"Vertex") -> tuple: 181 """Method to merge the edges that meet at the supplied Vertex. It is 182 assumed that only two tiles are impacted this one, and its neighbour across 183 the Edge on which v lies. Both are updated. For this reason the work is 184 delegated to get_updated_edges_from_merge which is run on both affected 185 tiles, but only determines the edges to remove and the new edge to be added 186 once. See that method for details. 187 188 Args: 189 v (Vertex): Vertex at which to merge Edges. This should currently be an 190 end 191 192 Returns: 193 tuple: 2 item list of the edge IDs to be removed and a new Edge object to 194 be added by the calling context (i.e. the containing Topology). 195 """ 196 to_remove, new_edge = self.get_updated_edges_from_merge(v) 197 if len(v.tiles) > 1: 198 v.tiles[1].get_updated_edges_from_merge(v, new_edge) 199 return to_remove, new_edge
Method to merge the edges that meet at the supplied Vertex. It is assumed that only two tiles are impacted this one, and its neighbour across the Edge on which v lies. Both are updated. For this reason the work is delegated to get_updated_edges_from_merge which is run on both affected tiles, but only determines the edges to remove and the new edge to be added once. See that method for details.
Args: v (Vertex): Vertex at which to merge Edges. This should currently be an end
Returns: tuple: 2 item list of the edge IDs to be removed and a new Edge object to be added by the calling context (i.e. the containing Topology).
201 def get_updated_edges_from_merge(self, v:"Vertex", new_edge:"Edge" = None): 202 """Updates the edges and edges_CW attributes based on insertion of 203 the supplied Vertex. If new_edge is supplied then the neighbour tile at 204 v has already created the needed new Edge and this Edge is the one that 205 will be 'slotted in' at the appropriate spot in the edges list. 206 207 The edges_CW is also updated to maintain correct directions of the edges. 208 The corners attribute is unaffected by these changes. 209 210 Args: 211 v (Vertex): existing Vertex at which to carry out the merge. 212 new_edge (Edge, optional): if another Tile has already carried out this 213 merge this should be the resulting new Edge for insertion into this 214 Tile. Defaults to None (when the new Edge will be constructed). 215 216 Returns: 217 Union[None, tuple]: either None (if a new edge was supplied) or a tuple 218 of the two edge IDs to be removed and the new edge added for return to 219 the calling context (i.e. the containing Topology). 220 """ 221 # get the two edge list index positions in which Vertex v is found 222 i, j = self.get_edge_IDs_including_vertex(v) 223 if new_edge is None: # then we must make a new one 224 # also record existing edge IDs to be removed 225 to_remove = [self.edges[i].ID, self.edges[j].ID] 226 new_edge = self.get_merged_edge(i, j) 227 return_edge_updates = True 228 else: 229 return_edge_updates = False 230 if abs(i - j) != 1: 231 # edge indices 'wrap' around from end of edge list to start so drop 232 # first and last current edges and stick new one on at the end 233 self.edges = self.edges[1:-1] + [new_edge] 234 else: 235 # insert new edge into list in place of the two old ones 236 self.edges = self.edges[:i] + [new_edge] + self.edges[j+1:] 237 # update the edge directions 238 self.set_edge_directions() 239 if return_edge_updates: 240 return to_remove, new_edge 241 else: 242 return None
Updates the edges and edges_CW attributes based on insertion of the supplied Vertex. If new_edge is supplied then the neighbour tile at v has already created the needed new Edge and this Edge is the one that will be 'slotted in' at the appropriate spot in the edges list.
The edges_CW is also updated to maintain correct directions of the edges. The corners attribute is unaffected by these changes.
Args: v (Vertex): existing Vertex at which to carry out the merge. new_edge (Edge, optional): if another Tile has already carried out this merge this should be the resulting new Edge for insertion into this Tile. Defaults to None (when the new Edge will be constructed).
Returns: Union[None, tuple]: either None (if a new edge was supplied) or a tuple of the two edge IDs to be removed and the new edge added for return to the calling context (i.e. the containing Topology).
244 def get_edge_IDs_including_vertex(self, v:"Vertex") -> tuple[int]: 245 """Gets the (two) index positions of the edges that include supplied Vertex. 246 247 Args: 248 v (Vertex): Vertex of interest. 249 250 Returns: 251 tuple[int]: two index positions of Edges in edges list that contain v. 252 """ 253 return (i for i, e in enumerate(self.edges) if v.ID in e.ID)
Gets the (two) index positions of the edges that include supplied Vertex.
Args: v (Vertex): Vertex of interest.
Returns: tuple[int]: two index positions of Edges in edges list that contain v.
255 def get_merged_edge(self, i:int, j:int) -> "Edge": 256 """Returns the new edge resulting from merging the two existing edges at 257 index positions i and j in the edges list. For example, if the current list 258 of edge IDs was 259 260 (0 1 2) (4 2) (4 5) (5 0) 261 262 and the merge Vertex was #2, the resulting new edge is constructed from 263 vertices (0 1 2 4). 264 265 Returns: 266 Edge: the requested new Edge. 267 """ 268 # if i and j are not consecutive, then j is predecessor edge 269 if abs(i - j) != 1: 270 i, j = j, i 271 # get edges and their directions 272 ei, ej = self.edges[i], self.edges[j] 273 CWi, CWj = self.edges_CW[i], self.edges_CW[j] 274 # DON'T MESS WITH THIS!!! 275 # for predecessors (the head) we want everything including the Vertex 276 # where the merge is occurring; for successors (the tail) we want all but 277 # the first Vertex (which is the one where the merge is occurring). In both 278 # cases contingent on whether existing Edges are CW or CCW we may need to 279 # flip the Vertex sequence to ensure that the merge Vertex is in the middle 280 # of the new edge that will be created 281 head = ei.corners if CWi else ei.corners[::-1] 282 tail = ej.corners[1:] if CWj else ej.corners[::-1][1:] 283 v_sequence = (head if CWi else head[::-1]) + (tail if CWj else tail[::-1]) 284 return Edge(v_sequence)
Returns the new edge resulting from merging the two existing edges at index positions i and j in the edges list. For example, if the current list of edge IDs was
(0 1 2) (4 2) (4 5) (5 0)
and the merge Vertex was #2, the resulting new edge is constructed from vertices (0 1 2 4).
Returns: Edge: the requested new Edge.
286 def offset_corners(self, offset:int): 287 """Shifts shape, corners, edges, and edges_CW by an offset amount. This is 288 used to align tiles that are similar, which is required for correct 289 transfer of 'base' tile labelling on to 'radius 1' tiles during Topology 290 construction. 291 292 Args: 293 offset int: the number of positions to shift the lists. 294 """ 295 if not offset is None or offset == 0: 296 self.shape = tiling_utils.offset_polygon_corners(self.shape, offset) 297 self.corners = self.corners[offset:] + self.corners[:offset] 298 self.edges = self.edges[offset:] + self.edges[:offset] 299 self.edges_CW = self.edges_CW[offset:] + self.edges_CW[:offset]
Shifts shape, corners, edges, and edges_CW by an offset amount. This is used to align tiles that are similar, which is required for correct transfer of 'base' tile labelling on to 'radius 1' tiles during Topology construction.
Args: offset int: the number of positions to shift the lists.
301 def get_edge_label(self, e:"Edge") -> str: 302 """Returns edge label of specified edge. 303 304 Args: 305 e (Edge): Edge whose label is required. 306 307 Returns: 308 str: requested edge label. 309 """ 310 return self.edge_labels[self.get_edge_IDs().index(e.ID)]
Returns edge label of specified edge.
Args: e (Edge): Edge whose label is required.
Returns: str: requested edge label.
312 def get_corner_label(self, v:"Vertex") -> str: 313 """Returns corner label of specified corner. 314 315 Args: 316 v (Vertex): corner whose label is required. 317 318 Returns: 319 str: requested corner label. 320 """ 321 return self.edge_labels[self.get_corner_IDs().index(v.ID)]
Returns corner label of specified corner.
Args: v (Vertex): corner whose label is required.
Returns: str: requested corner label.
323 def get_vertex_label_positions(self) -> list[geom.Point]: 324 """Returns a viable location at which to position corner labels inside 325 tile shape. The method is convoluted because a negative buffer may remove 326 colinear corners resulting in fewer positions than we have corners in the 327 tile shape! 328 329 Returns: 330 list[geom.Point]: list of locations. 331 """ 332 d = (self.shape.area ** 0.5) / 8 333 c = self.centre 334 corners = [c.point for c in self.corners] 335 return [geom.LineString([p, c]).line_interpolate_point(d) for p in corners]
Returns a viable location at which to position corner labels inside tile shape. The method is convoluted because a negative buffer may remove colinear corners resulting in fewer positions than we have corners in the tile shape!
Returns: list[geom.Point]: list of locations.
337 def get_edge_label_positions(self) -> list[geom.Point]: 338 """Returns a reasonable location at which to position edge labels inside 339 tile shape. 340 341 Returns: 342 list[geom.Point]: list of locations 343 """ 344 d = (self.shape.area ** 0.5) / 8 345 c = self.centre 346 # note direction is important as edge might not be a simple line segment 347 sides = [e.get_geometry(CW) for e, CW in zip(self.edges, self.edges_CW)] 348 return [geom.LineString([s.centroid, c]).line_interpolate_point(d) 349 for s in sides]
Returns a reasonable location at which to position edge labels inside tile shape.
Returns: list[geom.Point]: list of locations
351 def angle_at(self, v:"Vertex") -> float: 352 """Returns interior angle at the specified corner (in degrees). 353 354 Args: 355 v (Vertex): corner where angle is requested. 356 357 Returns: 358 float: angle at corner v in degrees. 359 """ 360 i = self.corners.index(v) 361 n = len(self.corners) 362 return tiling_utils.get_inner_angle(self.corners[i-1].point, 363 self.corners[i].point, 364 self.corners[(i + 1) % n].point)
Returns interior angle at the specified corner (in degrees).
Args: v (Vertex): corner where angle is requested.
Returns: float: angle at corner v in degrees.
367class Vertex: 368 """Class to store attributes of a vertex in a tiling.""" 369 point: geom.Point 370 """point (geom.Point): point location of the vertex.""" 371 ID: int 372 """integer (mostly but not necessarily in sequence) of vertex keyed into the 373 points dictionary of the containing Topology.""" 374 tiles: list["Tile"] 375 """list of Tiles incident on this vertex.""" 376 neighbours: list[int] 377 """list of the immediately adjacent other corner IDs. Only required to 378 determine if a point is a tiling vertex (when it will have) three or more 379 neighbours, so only IDs are stored.""" 380 base_ID: int = 1_000_000 381 """ID of corresponding Vertex in the tileable base_unit""" 382 transitivity_class: int = None 383 """transitivity class of the vertex under symmetries of the tiling""" 384 label: str = "" 385 """the (upper case letter) label of the vertex under the symmetries of the 386 tiling.""" 387 is_tiling_vertex: bool = True 388 """is_tiling_vertex (bool): True if this is a tiling vertex, rather than a 389 tile corner. E.g., A below is a corner, not a tiling vertex. B is a tiling 390 vertex: 391 392 +-------+ 393 | 1 | 394 | A---B---+ 395 | | 2 | 396 +---C +---+ 397 | | 398 +---+""" 399 400 def __init__(self, point:geom.Point, ID:int): 401 """Class constructor. 402 403 Args: 404 point (geom.Point): point location of the vertex. 405 ID (int): a unique integer ID (which will be its key in the containing 406 Topology points dictionary). 407 """ 408 self.point = point 409 self.ID = ID 410 self.base_ID = self.ID 411 self.tiles = [] 412 self.neighbours = [] 413 414 def __str__(self) -> str: 415 """Returns string representation of Vertex. 416 417 Returns: 418 str: string including ID, point and list of incident Tiles. 419 """ 420 return f"Vertex {self.ID} at {self.point} Tiles: {self.get_tile_IDs()}" 421 422 def __repr__(self) -> str: 423 return str(self) 424 425 def get_tile_IDs(self) -> list[int]: 426 """Convenience method to return list of Tile IDs (not the Tiles themselves). 427 428 Returns: 429 list[int]: list of Tile IDs 430 """ 431 return [t.ID for t in self.tiles] 432 433 def add_tile(self, tile:Tile): 434 """Adds supplied Tile to the tiles list if it is not already present. 435 436 Args: 437 tile (Tile): Tile to add. 438 """ 439 if not tile in self.tiles: 440 self.tiles.append(tile) 441 442 def add_neighbour(self, vertex_id:int): 443 """Adds supplied ID to the neighbours list if it is not already present 444 445 Args: 446 vertex_id (int): ID to add to the neighbours list. 447 """ 448 if not vertex_id in self.neighbours: 449 self.neighbours.append(vertex_id) 450 451 def clockwise_order_incident_tiles(self): 452 """Reorders the tiles list clockwise (this is for dual tiling construction) 453 """ 454 cw_order = tiling_utils.order_of_pts_cw_around_centre( 455 [t.centre for t in self.tiles], self.point) 456 self.tiles = [self.tiles[i] for i in cw_order] 457 458 def is_interior(self) -> bool: 459 """Tests if vertex is completely enclosed by its incident Tiles. Based on 460 summing the interior angles of the incident tiles at this vertex. 461 462 Returns: 463 bool: True if vertex is completely enclosed by incident Tiles. 464 """ 465 return abs(360 - sum([t.angle_at(self) for t in self.tiles])) \ 466 < tiling_utils.RESOLUTION
Class to store attributes of a vertex in a tiling.
400 def __init__(self, point:geom.Point, ID:int): 401 """Class constructor. 402 403 Args: 404 point (geom.Point): point location of the vertex. 405 ID (int): a unique integer ID (which will be its key in the containing 406 Topology points dictionary). 407 """ 408 self.point = point 409 self.ID = ID 410 self.base_ID = self.ID 411 self.tiles = [] 412 self.neighbours = []
Class constructor.
Args: point (geom.Point): point location of the vertex. ID (int): a unique integer ID (which will be its key in the containing Topology points dictionary).
integer (mostly but not necessarily in sequence) of vertex keyed into the points dictionary of the containing Topology.
list of the immediately adjacent other corner IDs. Only required to determine if a point is a tiling vertex (when it will have) three or more neighbours, so only IDs are stored.
is_tiling_vertex (bool): True if this is a tiling vertex, rather than a tile corner. E.g., A below is a corner, not a tiling vertex. B is a tiling vertex:
+-------+
| 1 |
| A---B---+
| | 2 |
+---C +---+
| |
+---+
425 def get_tile_IDs(self) -> list[int]: 426 """Convenience method to return list of Tile IDs (not the Tiles themselves). 427 428 Returns: 429 list[int]: list of Tile IDs 430 """ 431 return [t.ID for t in self.tiles]
Convenience method to return list of Tile IDs (not the Tiles themselves).
Returns: list[int]: list of Tile IDs
433 def add_tile(self, tile:Tile): 434 """Adds supplied Tile to the tiles list if it is not already present. 435 436 Args: 437 tile (Tile): Tile to add. 438 """ 439 if not tile in self.tiles: 440 self.tiles.append(tile)
Adds supplied Tile to the tiles list if it is not already present.
Args: tile (Tile): Tile to add.
442 def add_neighbour(self, vertex_id:int): 443 """Adds supplied ID to the neighbours list if it is not already present 444 445 Args: 446 vertex_id (int): ID to add to the neighbours list. 447 """ 448 if not vertex_id in self.neighbours: 449 self.neighbours.append(vertex_id)
Adds supplied ID to the neighbours list if it is not already present
Args: vertex_id (int): ID to add to the neighbours list.
451 def clockwise_order_incident_tiles(self): 452 """Reorders the tiles list clockwise (this is for dual tiling construction) 453 """ 454 cw_order = tiling_utils.order_of_pts_cw_around_centre( 455 [t.centre for t in self.tiles], self.point) 456 self.tiles = [self.tiles[i] for i in cw_order]
Reorders the tiles list clockwise (this is for dual tiling construction)
458 def is_interior(self) -> bool: 459 """Tests if vertex is completely enclosed by its incident Tiles. Based on 460 summing the interior angles of the incident tiles at this vertex. 461 462 Returns: 463 bool: True if vertex is completely enclosed by incident Tiles. 464 """ 465 return abs(360 - sum([t.angle_at(self) for t in self.tiles])) \ 466 < tiling_utils.RESOLUTION
Tests if vertex is completely enclosed by its incident Tiles. Based on summing the interior angles of the incident tiles at this vertex.
Returns: bool: True if vertex is completely enclosed by incident Tiles.
469class Edge: 470 """Class to represent edges in a tiling (not tile sides) per the definitions 471 in Grünbaum and Shephard. 472 """ 473 ID: tuple[int] 474 """IDs of the vertices at ends of the edge. Used as key in the containing 475 Topology's edges dictionary.""" 476 vertices: list["Vertex"] 477 """two item list of the end vertices.""" 478 corners: list["Vertex"] 479 """list of all the vertices in the edge (including its end vertices). In a 480 'normal' edge to edge tiling corners and vertices will be identical.""" 481 right_tile: "Tile" = None 482 """the tile to the right of the edge traversed from its first to its last 483 vertex. Given clockwise winding default, all edges will have a right_tile.""" 484 left_tile: "Tile" = None 485 """the tile to the left of the edge traversed from its first to its last 486 vertex. Exterior edges of the tiles in a Topology will not have a left_tile. 487 """ 488 base_ID: tuple[int] = (1_000_000, 1_000_000) 489 """ID of corresponding edge in the base tileable""" 490 transitivity_class: int = None 491 """transitivity class of the edge under symmetries of the tiling""" 492 label: str = "" 493 """the (lower case letter) label of the edge under the symmetries of the 494 tiling.""" 495 496 def __init__(self, corners:list[Vertex]): 497 """Class constructor. Initialises the corners and vertices lists and sets ID 498 to (vertices[0].ID, vertices[1].ID). The vertices list is all the corners 499 with is_tiling_vertex property True -- Note that during initialisation the 500 default of this property is True until after the relations between tiles and 501 vertices have been determined. 502 503 Args: 504 corners (list[Vertex]): list of all corners along the edge. 505 """ 506 self.corners = corners 507 self.vertices = [v for v in self.corners if v.is_tiling_vertex] 508 self.ID = tuple(v.ID for v in self.vertices) 509 510 def __str__(self) -> str: 511 """Returns a string representation of the Edge. 512 513 Returns: 514 str: include ID and a list of corner vertex IDs. 515 """ 516 return f"Edge {self.ID} Corners: {[c.ID for c in self.corners]}" 517 518 def __repr__(self) -> str: 519 return str(self) 520 521 def get_corner_IDs(self) -> list[int]: 522 """Convenience method to get the IDs of edge corners. 523 524 Returns: 525 list[int]: IDs of all corners. 526 """ 527 return [c.ID for c in self.corners] 528 529 def get_vertex_IDs(self) -> list[int]: 530 """Convenience method to get IDs of edge vertices. 531 532 Returns: 533 list[int]: list of IDs of the vertices. 534 """ 535 return [v.ID for v in self.vertices] 536 537 def insert_vertex(self, v:"Vertex", predecessor:"Vertex") -> list["Edge"]: 538 """Inserts a vertex along this edge after the specified predecessor 539 Vertex and returns this edge modified and a new edge. 540 541 If the initial edge was (say) (0 1 2 5) and the predecessor was set to 1 542 the returned edges would be (0 1 v) and (v 2 5). 543 """ 544 i = self.corners.index(predecessor) 545 new_edge = Edge([v] + self.corners[(i+1):]) 546 if not self.right_tile is None: 547 new_edge.right_tile = self.right_tile 548 if not self.left_tile is None: 549 new_edge.left_tile = self.left_tile 550 self.corners = self.corners[:(i+1)] + [v] 551 self.vertices = [self.vertices[0], v] 552 self.ID = tuple(v.ID for v in self.vertices) 553 return [self, new_edge] 554 555 def get_geometry(self, forward = True) -> geom.LineString: 556 """Returns a geom.LineString representing the geometry (including all 557 corners) of this Edge, optionally starting at either end. 558 559 Args: 560 forward (bool, optional): if True the returned LineString starts at 561 corners[0], else at corners[-1]. Defaults to True. 562 563 Returns: 564 geom.LineString: the required LineString. 565 """ 566 if forward: 567 return geom.LineString([v.point for v in self.corners]) 568 else: 569 return geom.LineString([v.point for v in self.corners[::-1]]) 570 571 def get_topology(self, forward = True) -> geom.LineString: 572 """Returns a LineString connecting the first and last corners (i.e. the 573 vertices) of this tilin edge, optionally starting from either end. 574 575 Args: 576 forward (bool, optional): if True LineString starts at vertices[0], 577 else at vertices[1]. Defaults to True. 578 579 Returns: 580 geom.LineString: the required LineString. 581 """ 582 if forward: 583 return geom.LineString([v.point for v in self.vertices]) 584 else: 585 return geom.LineString([v.point for v in self.vertices[::-1]])
Class to represent edges in a tiling (not tile sides) per the definitions in Grünbaum and Shephard.
496 def __init__(self, corners:list[Vertex]): 497 """Class constructor. Initialises the corners and vertices lists and sets ID 498 to (vertices[0].ID, vertices[1].ID). The vertices list is all the corners 499 with is_tiling_vertex property True -- Note that during initialisation the 500 default of this property is True until after the relations between tiles and 501 vertices have been determined. 502 503 Args: 504 corners (list[Vertex]): list of all corners along the edge. 505 """ 506 self.corners = corners 507 self.vertices = [v for v in self.corners if v.is_tiling_vertex] 508 self.ID = tuple(v.ID for v in self.vertices)
Class constructor. Initialises the corners and vertices lists and sets ID to (vertices[0].ID, vertices[1].ID). The vertices list is all the corners with is_tiling_vertex property True -- Note that during initialisation the default of this property is True until after the relations between tiles and vertices have been determined.
Args: corners (list[Vertex]): list of all corners along the edge.
IDs of the vertices at ends of the edge. Used as key in the containing Topology's edges dictionary.
list of all the vertices in the edge (including its end vertices). In a 'normal' edge to edge tiling corners and vertices will be identical.
the tile to the right of the edge traversed from its first to its last vertex. Given clockwise winding default, all edges will have a right_tile.
the tile to the left of the edge traversed from its first to its last vertex. Exterior edges of the tiles in a Topology will not have a left_tile.
521 def get_corner_IDs(self) -> list[int]: 522 """Convenience method to get the IDs of edge corners. 523 524 Returns: 525 list[int]: IDs of all corners. 526 """ 527 return [c.ID for c in self.corners]
Convenience method to get the IDs of edge corners.
Returns: list[int]: IDs of all corners.
529 def get_vertex_IDs(self) -> list[int]: 530 """Convenience method to get IDs of edge vertices. 531 532 Returns: 533 list[int]: list of IDs of the vertices. 534 """ 535 return [v.ID for v in self.vertices]
Convenience method to get IDs of edge vertices.
Returns: list[int]: list of IDs of the vertices.
537 def insert_vertex(self, v:"Vertex", predecessor:"Vertex") -> list["Edge"]: 538 """Inserts a vertex along this edge after the specified predecessor 539 Vertex and returns this edge modified and a new edge. 540 541 If the initial edge was (say) (0 1 2 5) and the predecessor was set to 1 542 the returned edges would be (0 1 v) and (v 2 5). 543 """ 544 i = self.corners.index(predecessor) 545 new_edge = Edge([v] + self.corners[(i+1):]) 546 if not self.right_tile is None: 547 new_edge.right_tile = self.right_tile 548 if not self.left_tile is None: 549 new_edge.left_tile = self.left_tile 550 self.corners = self.corners[:(i+1)] + [v] 551 self.vertices = [self.vertices[0], v] 552 self.ID = tuple(v.ID for v in self.vertices) 553 return [self, new_edge]
Inserts a vertex along this edge after the specified predecessor Vertex and returns this edge modified and a new edge.
If the initial edge was (say) (0 1 2 5) and the predecessor was set to 1 the returned edges would be (0 1 v) and (v 2 5).
555 def get_geometry(self, forward = True) -> geom.LineString: 556 """Returns a geom.LineString representing the geometry (including all 557 corners) of this Edge, optionally starting at either end. 558 559 Args: 560 forward (bool, optional): if True the returned LineString starts at 561 corners[0], else at corners[-1]. Defaults to True. 562 563 Returns: 564 geom.LineString: the required LineString. 565 """ 566 if forward: 567 return geom.LineString([v.point for v in self.corners]) 568 else: 569 return geom.LineString([v.point for v in self.corners[::-1]])
Returns a geom.LineString representing the geometry (including all corners) of this Edge, optionally starting at either end.
Args: forward (bool, optional): if True the returned LineString starts at corners[0], else at corners[-1]. Defaults to True.
Returns: geom.LineString: the required LineString.
571 def get_topology(self, forward = True) -> geom.LineString: 572 """Returns a LineString connecting the first and last corners (i.e. the 573 vertices) of this tilin edge, optionally starting from either end. 574 575 Args: 576 forward (bool, optional): if True LineString starts at vertices[0], 577 else at vertices[1]. Defaults to True. 578 579 Returns: 580 geom.LineString: the required LineString. 581 """ 582 if forward: 583 return geom.LineString([v.point for v in self.vertices]) 584 else: 585 return geom.LineString([v.point for v in self.vertices[::-1]])
Returns a LineString connecting the first and last corners (i.e. the vertices) of this tilin edge, optionally starting from either end.
Args: forward (bool, optional): if True LineString starts at vertices[0], else at vertices[1]. Defaults to True.
Returns: geom.LineString: the required LineString.