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]
@dataclass
class WeaveUnit(weavingspace.tileable.Tileable):
 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.

WeaveUnit(**kwargs)
58  def __init__(self, **kwargs):
59    super(WeaveUnit, self).__init__(**kwargs)
60    self.weave_type = self.weave_type.lower()
weave_type: str = 'plain'

type of weave pattern, one of plain, twill, basket, cube, hex or this. Defaults to plain.

aspect: float = 1.0

width of strands relative to the spacing. Defaults to 1.0.

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.

strands: str = 'a|b|c'

specification of the strand labels along each axis. Defaults to a|b|c.