weavingspace.weave_unit
The WeaveUnit
subclass of weavingspace.tileable.Tileable
implements
tileable geometric patterns constructed by specifying 2- or 3-axial weaves.
Examples: Explain usage here...
1#!/usr/bin/env python 2# coding: utf-8 3 4"""The `WeaveUnit` subclass of `weavingspace.tileable.Tileable` implements 5tileable geometric patterns constructed by specifying 2- or 3-axial weaves. 6 7Examples: 8 Explain usage here... 9""" 10 11import logging 12import itertools 13from dataclasses import dataclass 14from typing import Iterable, Union 15from collections import defaultdict 16 17import pandas as pd 18import geopandas as gpd 19import numpy as np 20import shapely.geometry as geom 21import shapely.affinity as affine 22import shapely.ops 23 24import weavingspace.weave_matrices as weave_matrices 25import weavingspace.tiling_utils as tiling_utils 26 27from weavingspace._loom import Loom 28from weavingspace._weave_grid import WeaveGrid 29 30from weavingspace.tileable import TileShape 31from weavingspace.tileable import Tileable 32 33 34@dataclass 35class WeaveUnit(Tileable): 36 """Extends Tileable to allow for tiles that appear like woven patterns. 37 """ 38 39 weave_type:str = "plain" 40 """type of weave pattern, one of `plain`, `twill`, `basket`, `cube`, `hex` or 41 `this`. Defaults to `plain`.""" 42 aspect:float = 1. 43 """width of strands relative to the `spacing`. Defaults to 1.0.""" 44 n:Union[int, tuple[int]] = (2, 2) 45 """number of over-under strands in biaxial weaves. Only one item is 46 required in a plain weave. Twill and basket patterns expect an even number of 47 entries in the tuple.""" 48 strands:str = "a|b|c" 49 """specification of the strand labels along each axis. Defaults to `a|b|c`.""" 50 _tie_up:np.ndarray = None 51 """optional tie-up array to pass through for `this` weave type.""" 52 _tr:np.ndarray = None 53 """optional treadling array to pass through for `this` weave type.""" 54 _th:np.ndarray = None 55 """optional threading array to pass through for `this` weave type.""" 56 57 def __init__(self, **kwargs): 58 super(WeaveUnit, self).__init__(**kwargs) 59 self.weave_type = self.weave_type.lower() 60 61 62 def _setup_tiles(self) -> None: 63 """Returns dictionary with weave unit and tile GeoDataFrames based on 64 parameters already supplied to the constructor. 65 """ 66 self._parameter_info() 67 68 if self.weave_type in ("hex", "cube"): 69 self.base_shape = TileShape.HEXAGON 70 self._setup_triaxial_weave_unit() 71 else: 72 self.base_shape = TileShape.RECTANGLE 73 self._setup_biaxial_weave_unit() 74 return 75 76 77 def _setup_regularised_prototile(self) -> None: 78 self.regularise_tiles() 79 if self.aspect < 1: 80 self.reattach_tiles() 81 self.regularised_prototile.geometry = tiling_utils.repair_polygon( 82 self.regularised_prototile.geometry) 83 84 85 def _parameter_info(self) -> None: 86 """Outputs logging message concerning the supplied aspect settings. 87 """ 88 if self.aspect == 0: 89 logging.info("Setting aspect to 0 is probably not a great plan.") 90 91 if self.aspect < 0 or self.aspect > 1: 92 logging.warning( 93 """Values of aspect outside the range 0 to 1 won't produce tiles 94 that will look like weaves, but they might be pretty anyway! Values 95 less than -1 seem particularly promising, especially with opacity 96 set less than 1.""") 97 98 return None 99 100 101 def _setup_biaxial_weave_unit(self) -> None: 102 """Returns weave tiles GeoDataFrame and tile GeoDataFrame in a 103 dictionary based on paramters already supplied to constructor. 104 """ 105 warp_threads, weft_threads, _ = \ 106 tiling_utils.get_strand_ids(self.strands) 107 108 if self.weave_type == "basket" and isinstance(self.n, (list, tuple)): 109 self.n = self.n[0] 110 111 p = weave_matrices.get_weave_pattern_matrix( 112 weave_type = self.weave_type, n = self.n, warp = warp_threads, 113 weft = weft_threads, tie_up = self._tie_up, tr = self._tr, 114 th = self._th) 115 116 self._make_shapes_from_coded_weave_matrix( 117 Loom(p), strand_labels = [weft_threads, warp_threads, []]) 118 119 120 def _get_triaxial_weave_matrices(self, 121 strands_1:Union[list[str],tuple[str]] = ["a"], 122 strands_2:Union[list[str],tuple[str]] = ["b"], 123 strands_3:Union[list[str],tuple[str]] = ["c"]) -> Loom: 124 """Returns encoded weave pattern matrix as Loom of three biaxial matrices. 125 126 Allowed weave_types: "cube" or "hex". 127 128 "hex" is not flexible and will fail with any strand label lists that are 129 not length 3 or include more than one non-blank "-" item. You can 130 generate the "hex" weave with the default settings in any case! 131 132 Strand lists should be length 3 or length 1. "cube" tolerates more 133 than "hex" for the items in the strand lists. 134 135 Defaults will produce 'mad weave'. 136 137 Args: 138 strands_1 (Union[list[str],tuple[str]], optional): list of labels 139 for warp strands. Defaults to ["a"]. 140 strands_2 (Union[list[str],tuple[str]], optional): list of labels 141 for weft strands. Defaults to ["b"]. 142 strands_3 (Union[list[str],tuple[str]], optional): list of labels 143 for weft strands. Defaults to ["c"]. 144 145 Returns: 146 Loom: which combines the three biaxial weaves 12, 23 and 31 implied 147 by the strand label lists. 148 """ 149 if self.weave_type == "hex": 150 loom = Loom( 151 weave_matrices.get_weave_pattern_matrix( 152 weave_type = "this", tie_up = np.ones((6, 6)), 153 warp = strands_1, weft = strands_2), 154 weave_matrices.get_weave_pattern_matrix( 155 weave_type = "this", tie_up = np.ones((6, 6)), 156 warp = strands_2, weft = strands_3), 157 weave_matrices.get_weave_pattern_matrix( 158 weave_type = "this", tie_up = np.ones((6, 6)), 159 warp = strands_3, weft = strands_1)) 160 else: # "cube" 161 loom = Loom( 162 # Note n = (1,2,1,2) is required here to force 6x6 twill 163 weave_matrices.get_weave_pattern_matrix( 164 weave_type = "twill", n = (1, 2, 1, 2), 165 warp = strands_1, weft = strands_2), 166 weave_matrices.get_weave_pattern_matrix( 167 weave_type = "twill", n = (1, 2, 1, 2), 168 warp = strands_2, weft = strands_3), 169 weave_matrices.get_weave_pattern_matrix( 170 weave_type = "twill", n = (1, 2, 1, 2), 171 warp = strands_3, weft = strands_1)) 172 return loom 173 174 175 def _setup_triaxial_weave_unit(self) -> None: 176 """Returns weave tiles GeoDataFrame and tile GeoDataFrame in a 177 dictionary based on parameters already supplied to constructor. 178 """ 179 strands_1, strands_2, strands_3 = \ 180 tiling_utils.get_strand_ids(self.strands) 181 182 loom = self._get_triaxial_weave_matrices( 183 strands_1 = strands_1, strands_2 = strands_2, strands_3 = strands_3) 184 185 self._make_shapes_from_coded_weave_matrix( 186 loom, strand_labels = [strands_1, strands_2, strands_3]) 187 188 189 def _make_shapes_from_coded_weave_matrix( 190 self, loom:Loom, strand_labels:list[list[str]] = [["a"], ["b"], ["c"]] 191 ) -> None: 192 """Returns weave tiles and prototile GeoDataFrames in a dictionary 193 194 Builds the geometries associated with a given weave supplied as 195 'loom' containing the coordinates in an appropriate grid (Cartesian or 196 triangular) and the orderings of the strands at each coordinate location 197 198 Args: 199 loom (Loom): matrix or stack of matrices representing the weave 200 pattern. 201 strand_labels (list[list[str]], optional): list of lists of labels 202 for strands in each direction. Defaults to [["a"], ["b"], ["c"]] 203 """ 204 grid = WeaveGrid(loom.n_axes, loom.orientations, self.spacing) 205 # expand the list of strand labels if needed in each direction 206 # labels = [] 207 labels = [thread * int(np.ceil(dim // len(thread))) 208 for dim, thread in zip(loom.dimensions, strand_labels)] 209 weave_polys = [] 210 strand_ids = [] 211 cells = [] 212 for k, strand_order in zip(loom.indices, loom.orderings): 213 ids = [thread[coord] for coord, thread in zip(k, labels)] 214 cells.append(grid.get_grid_cell_at(k)) 215 if strand_order is None: 216 continue # No strands present 217 if strand_order == "NA": 218 continue # Inconsistency in layer order 219 n_slices = [len(id) for id in ids] 220 next_polys = grid.get_visible_cell_strands( 221 width = self.aspect, coords = k, 222 strand_order = strand_order, n_slices = n_slices) 223 weave_polys.extend(next_polys) 224 next_labels = [list(ids[i]) for i in strand_order] # list of lists 225 next_labels = list(itertools.chain(*next_labels)) # flatten 226 strand_ids.extend(next_labels) 227 # sometimes empty polygons make it to here, so 228 # filter those out along with the associated IDs 229 real_polys = [not p.is_empty for p in weave_polys] 230 weave_polys = [p for p, b in zip(weave_polys, real_polys) if b] 231 strand_ids = [id for id, b in zip(strand_ids, real_polys) if b] 232 # note that the tile is important for the biaxial case, which may 233 # not be centred on (0, 0) 234 tile = tiling_utils.safe_union(gpd.GeoSeries(cells), as_polygon = True) 235 shift = (-tile.centroid.x, -tile.centroid.y) if loom.n_axes == 2 else (0, 0) 236 tile = grid.get_tile_from_cells(tile) 237 self.tiles = self._get_weave_tiles_gdf(weave_polys, strand_ids, shift) 238 self.prototile = gpd.GeoDataFrame(geometry = gpd.GeoSeries([ 239 tiling_utils.get_clean_polygon(tile)]), crs = self.crs) 240 # self.setup_vectors() 241 return None 242 243 244 def _get_weave_tiles_gdf( 245 self, polys:list[geom.Polygon], strand_ids:list[str], 246 offset:tuple[float]) -> gpd.GeoDataFrame: 247 """Makes a GeoDataFrame from weave tile polygons, labels, etc. 248 249 Args: 250 polys (list[Polygon | MultiPolygon]): list of weave tile 251 polygons. 252 strand_ids (list[str]): list of strand labels. 253 offset (tuple[float]): offset to centre the weave tiles on the 254 tile. 255 256 Returns: 257 geopandas.GeoDataFrame: GeoDataFrame clipped to the tile, with 258 margin applied. 259 """ 260 weave = gpd.GeoDataFrame( 261 data = {"tile_id": strand_ids}, 262 geometry = gpd.GeoSeries([affine.translate(p, offset[0], offset[1]) 263 for p in polys])) 264 weave = weave[weave.tile_id != "-"] 265 266 # some buffering is required if aspect is 1 to safely dissolve and 267 # explode weave unit tiles that meet at corners 268 if self.aspect == 1: 269 # grow for dissolve 270 weave.geometry = weave.geometry.buffer( 271 self.spacing * tiling_utils.RESOLUTION, 272 join_style = 2, cap_style = 3) 273 weave = weave.dissolve(by = "tile_id", as_index = False) 274 # shrink by more to explode into separate polygons 275 weave.geometry = weave.geometry.buffer( 276 -2 * self.spacing * tiling_utils.RESOLUTION, 277 join_style = 2, cap_style = 3) 278 weave = weave.explode(ignore_index = True) 279 weave.geometry = weave.geometry.buffer( 280 self.spacing * tiling_utils.RESOLUTION, 281 join_style = 2, cap_style = 3) 282 else: # aspect < 1 is fine without buffering 283 weave = weave.dissolve(by = "tile_id", as_index = False) 284 weave = weave.explode(ignore_index = True) 285 286 weave.geometry = gpd.GeoSeries( 287 [tiling_utils.get_clean_polygon(p) for p in weave.geometry]) 288 return weave.set_crs(self.crs) 289 290 291 def _get_axis_from_label(self, label:str = "a", strands:str = None): 292 """Determines the axis of a tile_id from the strands spec string. 293 294 Args: 295 label (str, optional): the tile_id. Defaults to "a". 296 strands (str, optional): the strand spec. Defaults to the WeaveUnit 297 strands attribute. 298 299 Returns: 300 _type_: the axis in which the supplied tile is found. 301 """ 302 if strands == None: 303 strands = self.strands 304 index = strands.index(label) 305 return strands[:index].count("|") 306 307 308 def _get_legend_tiles(self) -> gpd.GeoDataFrame: 309 """Returns tiles suitable for use in a legend representation. 310 311 One tile for each tile_id value will be chosen, close to the 312 centre of the prototile extent, and not among the smallest tiles present 313 (for example not a short length of strand mostly hidden by other 314 strands) 315 316 Returns: 317 gpd.GeoDataFrame: the chosen tiles. 318 """ 319 angles = ((0, 240, 120) 320 if self.weave_type in ("hex", "cube") 321 else (90, 0)) 322 tile_ids = pd.Series.unique(self.tiles.tile_id) 323 groups = self.tiles.groupby("tile_id") 324 tiles, rotations = [], [] 325 for id in tile_ids: 326 candidates = groups.get_group(id) 327 axis = self._get_axis_from_label(id, self.strands) 328 tiles.append(self._get_most_central_large_tile( 329 candidates, tiles)) 330 rotations.append(-angles[axis] + self.rotation) 331 return gpd.GeoDataFrame( 332 data = {"tile_id": tile_ids, "rotation": rotations}, 333 crs = self.crs, 334 geometry = gpd.GeoSeries(tiles)) 335 336 337 def _get_most_central_large_tile(self, tiles:gpd.GeoDataFrame, 338 other_tiles:list[geom.Polygon]) -> geom.Polygon: 339 """Gets a large tile close to the centre of the WeaveUnit. 340 341 Args: 342 tiles (gpd.GeoDataFrame): the set of tiles to choose from. 343 344 Returns: 345 geom.Polygon: the chosen, large central tile. 346 """ 347 areas = [g.area for g in tiles.geometry] 348 min_area, max_area = min(areas), max(areas) 349 if min_area / max_area > 0.5: 350 geoms = list(tiles.geometry) 351 else: 352 mean_log_a = np.mean(np.log(areas)) 353 geoms = [g for g, a in zip(tiles.geometry, areas) 354 if np.log(a) > mean_log_a] 355 if len(other_tiles) == 0 or self.weave_type in ("cube", "hex"): 356 d = [g.centroid.distance(geom.Point(0, 0)) for g in geoms] 357 else: 358 c = geom.MultiPolygon(other_tiles).centroid 359 d = [geom.MultiPolygon([g] + other_tiles).centroid.distance(c) 360 for g in geoms] 361 return geoms[d.index(min(d))] 362 363 364 def _get_legend_key_shapes(self, polygon:geom.Polygon, 365 counts:Iterable = [1] * 25, angle:float = 0, 366 radial:bool = False) -> list[geom.Polygon]: 367 """Returns a list of polygons obtained by slicing the supplied polygon 368 across its length into n slices. Orientation of the polygon is 369 indicated by the angle. 370 371 The returned list of polygons can be used to form a colour ramp in a 372 legend. 373 374 Args: 375 polygon (geom.Polygon): the weave strand polygon to slice. 376 counts (Iterable, optional): an iterable list of the numbers of 377 slices in each category. Defaults to [1] * 25. 378 angle (float, optional): orientation of the polygon. Defaults to 0. 379 categorical (bool, optional): ignored by WeaveUnit. 380 381 Returns: 382 list[geom.Polygon]: a list of polygons. 383 """ 384 c = polygon.centroid 385 g = affine.rotate(polygon, -angle, origin = c) 386 width, height, left, bottom = \ 387 tiling_utils.get_width_height_left_bottom(gpd.GeoSeries([g])) 388 total = sum(counts) 389 cuts = list(np.cumsum(counts)) 390 cuts = [0] + [c / total for c in cuts] 391 cuts = [left + c * width for c in cuts] 392 # add margin to avoid weird effects intersecting almost parallel lines. 393 cuts[0] = cuts[0] - 1 394 cuts[-1] = cuts[-1] + 1 395 bottom = bottom - 1 396 top = bottom + height + 1 397 slices = [] 398 for l, r in zip(cuts[:-1], cuts[1:]): 399 slice = geom.Polygon([(l, bottom), (r, bottom), (r, top), (l, top)]) 400 slices.append(slice.intersection(g)) 401 return [affine.rotate(s, angle, origin = c) for s in slices]
35@dataclass 36class WeaveUnit(Tileable): 37 """Extends Tileable to allow for tiles that appear like woven patterns. 38 """ 39 40 weave_type:str = "plain" 41 """type of weave pattern, one of `plain`, `twill`, `basket`, `cube`, `hex` or 42 `this`. Defaults to `plain`.""" 43 aspect:float = 1. 44 """width of strands relative to the `spacing`. Defaults to 1.0.""" 45 n:Union[int, tuple[int]] = (2, 2) 46 """number of over-under strands in biaxial weaves. Only one item is 47 required in a plain weave. Twill and basket patterns expect an even number of 48 entries in the tuple.""" 49 strands:str = "a|b|c" 50 """specification of the strand labels along each axis. Defaults to `a|b|c`.""" 51 _tie_up:np.ndarray = None 52 """optional tie-up array to pass through for `this` weave type.""" 53 _tr:np.ndarray = None 54 """optional treadling array to pass through for `this` weave type.""" 55 _th:np.ndarray = None 56 """optional threading array to pass through for `this` weave type.""" 57 58 def __init__(self, **kwargs): 59 super(WeaveUnit, self).__init__(**kwargs) 60 self.weave_type = self.weave_type.lower() 61 62 63 def _setup_tiles(self) -> None: 64 """Returns dictionary with weave unit and tile GeoDataFrames based on 65 parameters already supplied to the constructor. 66 """ 67 self._parameter_info() 68 69 if self.weave_type in ("hex", "cube"): 70 self.base_shape = TileShape.HEXAGON 71 self._setup_triaxial_weave_unit() 72 else: 73 self.base_shape = TileShape.RECTANGLE 74 self._setup_biaxial_weave_unit() 75 return 76 77 78 def _setup_regularised_prototile(self) -> None: 79 self.regularise_tiles() 80 if self.aspect < 1: 81 self.reattach_tiles() 82 self.regularised_prototile.geometry = tiling_utils.repair_polygon( 83 self.regularised_prototile.geometry) 84 85 86 def _parameter_info(self) -> None: 87 """Outputs logging message concerning the supplied aspect settings. 88 """ 89 if self.aspect == 0: 90 logging.info("Setting aspect to 0 is probably not a great plan.") 91 92 if self.aspect < 0 or self.aspect > 1: 93 logging.warning( 94 """Values of aspect outside the range 0 to 1 won't produce tiles 95 that will look like weaves, but they might be pretty anyway! Values 96 less than -1 seem particularly promising, especially with opacity 97 set less than 1.""") 98 99 return None 100 101 102 def _setup_biaxial_weave_unit(self) -> None: 103 """Returns weave tiles GeoDataFrame and tile GeoDataFrame in a 104 dictionary based on paramters already supplied to constructor. 105 """ 106 warp_threads, weft_threads, _ = \ 107 tiling_utils.get_strand_ids(self.strands) 108 109 if self.weave_type == "basket" and isinstance(self.n, (list, tuple)): 110 self.n = self.n[0] 111 112 p = weave_matrices.get_weave_pattern_matrix( 113 weave_type = self.weave_type, n = self.n, warp = warp_threads, 114 weft = weft_threads, tie_up = self._tie_up, tr = self._tr, 115 th = self._th) 116 117 self._make_shapes_from_coded_weave_matrix( 118 Loom(p), strand_labels = [weft_threads, warp_threads, []]) 119 120 121 def _get_triaxial_weave_matrices(self, 122 strands_1:Union[list[str],tuple[str]] = ["a"], 123 strands_2:Union[list[str],tuple[str]] = ["b"], 124 strands_3:Union[list[str],tuple[str]] = ["c"]) -> Loom: 125 """Returns encoded weave pattern matrix as Loom of three biaxial matrices. 126 127 Allowed weave_types: "cube" or "hex". 128 129 "hex" is not flexible and will fail with any strand label lists that are 130 not length 3 or include more than one non-blank "-" item. You can 131 generate the "hex" weave with the default settings in any case! 132 133 Strand lists should be length 3 or length 1. "cube" tolerates more 134 than "hex" for the items in the strand lists. 135 136 Defaults will produce 'mad weave'. 137 138 Args: 139 strands_1 (Union[list[str],tuple[str]], optional): list of labels 140 for warp strands. Defaults to ["a"]. 141 strands_2 (Union[list[str],tuple[str]], optional): list of labels 142 for weft strands. Defaults to ["b"]. 143 strands_3 (Union[list[str],tuple[str]], optional): list of labels 144 for weft strands. Defaults to ["c"]. 145 146 Returns: 147 Loom: which combines the three biaxial weaves 12, 23 and 31 implied 148 by the strand label lists. 149 """ 150 if self.weave_type == "hex": 151 loom = Loom( 152 weave_matrices.get_weave_pattern_matrix( 153 weave_type = "this", tie_up = np.ones((6, 6)), 154 warp = strands_1, weft = strands_2), 155 weave_matrices.get_weave_pattern_matrix( 156 weave_type = "this", tie_up = np.ones((6, 6)), 157 warp = strands_2, weft = strands_3), 158 weave_matrices.get_weave_pattern_matrix( 159 weave_type = "this", tie_up = np.ones((6, 6)), 160 warp = strands_3, weft = strands_1)) 161 else: # "cube" 162 loom = Loom( 163 # Note n = (1,2,1,2) is required here to force 6x6 twill 164 weave_matrices.get_weave_pattern_matrix( 165 weave_type = "twill", n = (1, 2, 1, 2), 166 warp = strands_1, weft = strands_2), 167 weave_matrices.get_weave_pattern_matrix( 168 weave_type = "twill", n = (1, 2, 1, 2), 169 warp = strands_2, weft = strands_3), 170 weave_matrices.get_weave_pattern_matrix( 171 weave_type = "twill", n = (1, 2, 1, 2), 172 warp = strands_3, weft = strands_1)) 173 return loom 174 175 176 def _setup_triaxial_weave_unit(self) -> None: 177 """Returns weave tiles GeoDataFrame and tile GeoDataFrame in a 178 dictionary based on parameters already supplied to constructor. 179 """ 180 strands_1, strands_2, strands_3 = \ 181 tiling_utils.get_strand_ids(self.strands) 182 183 loom = self._get_triaxial_weave_matrices( 184 strands_1 = strands_1, strands_2 = strands_2, strands_3 = strands_3) 185 186 self._make_shapes_from_coded_weave_matrix( 187 loom, strand_labels = [strands_1, strands_2, strands_3]) 188 189 190 def _make_shapes_from_coded_weave_matrix( 191 self, loom:Loom, strand_labels:list[list[str]] = [["a"], ["b"], ["c"]] 192 ) -> None: 193 """Returns weave tiles and prototile GeoDataFrames in a dictionary 194 195 Builds the geometries associated with a given weave supplied as 196 'loom' containing the coordinates in an appropriate grid (Cartesian or 197 triangular) and the orderings of the strands at each coordinate location 198 199 Args: 200 loom (Loom): matrix or stack of matrices representing the weave 201 pattern. 202 strand_labels (list[list[str]], optional): list of lists of labels 203 for strands in each direction. Defaults to [["a"], ["b"], ["c"]] 204 """ 205 grid = WeaveGrid(loom.n_axes, loom.orientations, self.spacing) 206 # expand the list of strand labels if needed in each direction 207 # labels = [] 208 labels = [thread * int(np.ceil(dim // len(thread))) 209 for dim, thread in zip(loom.dimensions, strand_labels)] 210 weave_polys = [] 211 strand_ids = [] 212 cells = [] 213 for k, strand_order in zip(loom.indices, loom.orderings): 214 ids = [thread[coord] for coord, thread in zip(k, labels)] 215 cells.append(grid.get_grid_cell_at(k)) 216 if strand_order is None: 217 continue # No strands present 218 if strand_order == "NA": 219 continue # Inconsistency in layer order 220 n_slices = [len(id) for id in ids] 221 next_polys = grid.get_visible_cell_strands( 222 width = self.aspect, coords = k, 223 strand_order = strand_order, n_slices = n_slices) 224 weave_polys.extend(next_polys) 225 next_labels = [list(ids[i]) for i in strand_order] # list of lists 226 next_labels = list(itertools.chain(*next_labels)) # flatten 227 strand_ids.extend(next_labels) 228 # sometimes empty polygons make it to here, so 229 # filter those out along with the associated IDs 230 real_polys = [not p.is_empty for p in weave_polys] 231 weave_polys = [p for p, b in zip(weave_polys, real_polys) if b] 232 strand_ids = [id for id, b in zip(strand_ids, real_polys) if b] 233 # note that the tile is important for the biaxial case, which may 234 # not be centred on (0, 0) 235 tile = tiling_utils.safe_union(gpd.GeoSeries(cells), as_polygon = True) 236 shift = (-tile.centroid.x, -tile.centroid.y) if loom.n_axes == 2 else (0, 0) 237 tile = grid.get_tile_from_cells(tile) 238 self.tiles = self._get_weave_tiles_gdf(weave_polys, strand_ids, shift) 239 self.prototile = gpd.GeoDataFrame(geometry = gpd.GeoSeries([ 240 tiling_utils.get_clean_polygon(tile)]), crs = self.crs) 241 # self.setup_vectors() 242 return None 243 244 245 def _get_weave_tiles_gdf( 246 self, polys:list[geom.Polygon], strand_ids:list[str], 247 offset:tuple[float]) -> gpd.GeoDataFrame: 248 """Makes a GeoDataFrame from weave tile polygons, labels, etc. 249 250 Args: 251 polys (list[Polygon | MultiPolygon]): list of weave tile 252 polygons. 253 strand_ids (list[str]): list of strand labels. 254 offset (tuple[float]): offset to centre the weave tiles on the 255 tile. 256 257 Returns: 258 geopandas.GeoDataFrame: GeoDataFrame clipped to the tile, with 259 margin applied. 260 """ 261 weave = gpd.GeoDataFrame( 262 data = {"tile_id": strand_ids}, 263 geometry = gpd.GeoSeries([affine.translate(p, offset[0], offset[1]) 264 for p in polys])) 265 weave = weave[weave.tile_id != "-"] 266 267 # some buffering is required if aspect is 1 to safely dissolve and 268 # explode weave unit tiles that meet at corners 269 if self.aspect == 1: 270 # grow for dissolve 271 weave.geometry = weave.geometry.buffer( 272 self.spacing * tiling_utils.RESOLUTION, 273 join_style = 2, cap_style = 3) 274 weave = weave.dissolve(by = "tile_id", as_index = False) 275 # shrink by more to explode into separate polygons 276 weave.geometry = weave.geometry.buffer( 277 -2 * self.spacing * tiling_utils.RESOLUTION, 278 join_style = 2, cap_style = 3) 279 weave = weave.explode(ignore_index = True) 280 weave.geometry = weave.geometry.buffer( 281 self.spacing * tiling_utils.RESOLUTION, 282 join_style = 2, cap_style = 3) 283 else: # aspect < 1 is fine without buffering 284 weave = weave.dissolve(by = "tile_id", as_index = False) 285 weave = weave.explode(ignore_index = True) 286 287 weave.geometry = gpd.GeoSeries( 288 [tiling_utils.get_clean_polygon(p) for p in weave.geometry]) 289 return weave.set_crs(self.crs) 290 291 292 def _get_axis_from_label(self, label:str = "a", strands:str = None): 293 """Determines the axis of a tile_id from the strands spec string. 294 295 Args: 296 label (str, optional): the tile_id. Defaults to "a". 297 strands (str, optional): the strand spec. Defaults to the WeaveUnit 298 strands attribute. 299 300 Returns: 301 _type_: the axis in which the supplied tile is found. 302 """ 303 if strands == None: 304 strands = self.strands 305 index = strands.index(label) 306 return strands[:index].count("|") 307 308 309 def _get_legend_tiles(self) -> gpd.GeoDataFrame: 310 """Returns tiles suitable for use in a legend representation. 311 312 One tile for each tile_id value will be chosen, close to the 313 centre of the prototile extent, and not among the smallest tiles present 314 (for example not a short length of strand mostly hidden by other 315 strands) 316 317 Returns: 318 gpd.GeoDataFrame: the chosen tiles. 319 """ 320 angles = ((0, 240, 120) 321 if self.weave_type in ("hex", "cube") 322 else (90, 0)) 323 tile_ids = pd.Series.unique(self.tiles.tile_id) 324 groups = self.tiles.groupby("tile_id") 325 tiles, rotations = [], [] 326 for id in tile_ids: 327 candidates = groups.get_group(id) 328 axis = self._get_axis_from_label(id, self.strands) 329 tiles.append(self._get_most_central_large_tile( 330 candidates, tiles)) 331 rotations.append(-angles[axis] + self.rotation) 332 return gpd.GeoDataFrame( 333 data = {"tile_id": tile_ids, "rotation": rotations}, 334 crs = self.crs, 335 geometry = gpd.GeoSeries(tiles)) 336 337 338 def _get_most_central_large_tile(self, tiles:gpd.GeoDataFrame, 339 other_tiles:list[geom.Polygon]) -> geom.Polygon: 340 """Gets a large tile close to the centre of the WeaveUnit. 341 342 Args: 343 tiles (gpd.GeoDataFrame): the set of tiles to choose from. 344 345 Returns: 346 geom.Polygon: the chosen, large central tile. 347 """ 348 areas = [g.area for g in tiles.geometry] 349 min_area, max_area = min(areas), max(areas) 350 if min_area / max_area > 0.5: 351 geoms = list(tiles.geometry) 352 else: 353 mean_log_a = np.mean(np.log(areas)) 354 geoms = [g for g, a in zip(tiles.geometry, areas) 355 if np.log(a) > mean_log_a] 356 if len(other_tiles) == 0 or self.weave_type in ("cube", "hex"): 357 d = [g.centroid.distance(geom.Point(0, 0)) for g in geoms] 358 else: 359 c = geom.MultiPolygon(other_tiles).centroid 360 d = [geom.MultiPolygon([g] + other_tiles).centroid.distance(c) 361 for g in geoms] 362 return geoms[d.index(min(d))] 363 364 365 def _get_legend_key_shapes(self, polygon:geom.Polygon, 366 counts:Iterable = [1] * 25, angle:float = 0, 367 radial:bool = False) -> list[geom.Polygon]: 368 """Returns a list of polygons obtained by slicing the supplied polygon 369 across its length into n slices. Orientation of the polygon is 370 indicated by the angle. 371 372 The returned list of polygons can be used to form a colour ramp in a 373 legend. 374 375 Args: 376 polygon (geom.Polygon): the weave strand polygon to slice. 377 counts (Iterable, optional): an iterable list of the numbers of 378 slices in each category. Defaults to [1] * 25. 379 angle (float, optional): orientation of the polygon. Defaults to 0. 380 categorical (bool, optional): ignored by WeaveUnit. 381 382 Returns: 383 list[geom.Polygon]: a list of polygons. 384 """ 385 c = polygon.centroid 386 g = affine.rotate(polygon, -angle, origin = c) 387 width, height, left, bottom = \ 388 tiling_utils.get_width_height_left_bottom(gpd.GeoSeries([g])) 389 total = sum(counts) 390 cuts = list(np.cumsum(counts)) 391 cuts = [0] + [c / total for c in cuts] 392 cuts = [left + c * width for c in cuts] 393 # add margin to avoid weird effects intersecting almost parallel lines. 394 cuts[0] = cuts[0] - 1 395 cuts[-1] = cuts[-1] + 1 396 bottom = bottom - 1 397 top = bottom + height + 1 398 slices = [] 399 for l, r in zip(cuts[:-1], cuts[1:]): 400 slice = geom.Polygon([(l, bottom), (r, bottom), (r, top), (l, top)]) 401 slices.append(slice.intersection(g)) 402 return [affine.rotate(s, angle, origin = c) for s in slices]
Extends Tileable to allow for tiles that appear like woven patterns.
weave_type: str =
'plain'
type of weave pattern, one of plain
, twill
, basket
, cube
, hex
or
this
. Defaults to plain
.
n: Union[int, tuple[int]] =
(2, 2)
number of over-under strands in biaxial weaves. Only one item is required in a plain weave. Twill and basket patterns expect an even number of entries in the tuple.
Inherited Members
- weavingspace.tileable.Tileable
- tiles
- prototile
- spacing
- base_shape
- vectors
- regularised_prototile
- crs
- rotation
- debug
- setup_vectors
- get_vectors
- setup_regularised_prototile_from_tiles
- merge_fragments
- reattach_tiles
- regularise_tiles
- get_local_patch
- fit_tiles_to_prototile
- inset_tiles
- plot
- transform_scale
- transform_rotate
- transform_skew