weavingspace 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of weavingspace might be problematic. Click here for more details.

@@ -0,0 +1,19 @@
1
+ Copyright (c) 2022-25 David O'Sullivan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.2
2
+ Name: weavingspace
3
+ Version: 0.0.1
4
+ Summary: A package for making tiled mapping layers
5
+ Author-email: David O'Sullivan <osullivan512@gmail.com>
6
+ License: Copyright (c) 2022-25 David O'Sullivan
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ Project-URL: Homepage, https://github.com/DOSull/weaving-space
27
+ Classifier: Programming Language :: Python :: 3
28
+ Classifier: Operating System :: OS Independent
29
+ Requires-Python: >=3.12
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: requests
33
+ Requires-Dist: importlib-metadata; python_version >= "3.12"
34
+ Requires-Dist: geopandas
35
+ Requires-Dist: pandas
36
+ Requires-Dist: numpy
37
+ Requires-Dist: scipy
38
+ Requires-Dist: shapely
39
+ Requires-Dist: networkx
40
+ Requires-Dist: pillow
41
+ Requires-Dist: matplotlib
42
+ Requires-Dist: dataclasses
43
+ Requires-Dist: typing
44
+
45
+ # Geographical weaving
46
+ This is work in progress towards developing tiled geospatial data layers for symbolisation of complex multi-attribute choropleths.
47
+
48
+ The original proof-of-concept code is in R and Rmd files in the `r-stuff` folder. Needed datasets to run the code are in the `data` folder.
49
+
50
+ ## **Update** (*April 2022*)
51
+ There is now a working version in python using `geopandas` and `shapely`. It will be more extensible in the longer run and will be the basis of any further work at this stage. The python implementation also appears less prone to topological glitches when tiles are dissolved to form tilings and does not require that we use any additional libraries to get reasonable(ish) performance when tiling large maps. The python source is in the `weavingspace` folder.
52
+
53
+ There are several jupyter notebooks in this repo that show examples of how to use the codebase, and the API is documented [here](https://dosull.github.io/weaving-space/doc/weavingspace.html).
54
+
55
+ ## Talks
56
+ An overview of the concepts assembled from the proof-of-concept _R_ code is on [this webpage](https://dosull.github.io/weaving-space/NZCS-Nov-2021/make-weave-map.html). A similar follow-up talk is [available here](https://dosull.github.io/weaving-space/Palmerston-North-Nov-2022/slides/index.html)
57
+
58
+ Slides from a more recent talk explaining the work, extended to tiled maps (of which woven maps are a special case) is available [here](https://dosull.github.io/weaving-space/Palmerston-North-Nov-2022/slides/).
@@ -0,0 +1,14 @@
1
+ # Geographical weaving
2
+ This is work in progress towards developing tiled geospatial data layers for symbolisation of complex multi-attribute choropleths.
3
+
4
+ The original proof-of-concept code is in R and Rmd files in the `r-stuff` folder. Needed datasets to run the code are in the `data` folder.
5
+
6
+ ## **Update** (*April 2022*)
7
+ There is now a working version in python using `geopandas` and `shapely`. It will be more extensible in the longer run and will be the basis of any further work at this stage. The python implementation also appears less prone to topological glitches when tiles are dissolved to form tilings and does not require that we use any additional libraries to get reasonable(ish) performance when tiling large maps. The python source is in the `weavingspace` folder.
8
+
9
+ There are several jupyter notebooks in this repo that show examples of how to use the codebase, and the API is documented [here](https://dosull.github.io/weaving-space/doc/weavingspace.html).
10
+
11
+ ## Talks
12
+ An overview of the concepts assembled from the proof-of-concept _R_ code is on [this webpage](https://dosull.github.io/weaving-space/NZCS-Nov-2021/make-weave-map.html). A similar follow-up talk is [available here](https://dosull.github.io/weaving-space/Palmerston-North-Nov-2022/slides/index.html)
13
+
14
+ Slides from a more recent talk explaining the work, extended to tiled maps (of which woven maps are a special case) is available [here](https://dosull.github.io/weaving-space/Palmerston-North-Nov-2022/slides/).
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "weavingspace"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="David O'Sullivan", email="osullivan512@gmail.com" },
10
+ ]
11
+ description = "A package for making tiled mapping layers"
12
+ requires-python = ">=3.12"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+ readme = "README.md"
18
+ license = {file = "LICENSE"}
19
+ dependencies = [
20
+ "requests",
21
+ 'importlib-metadata; python_version>="3.12"',
22
+ "geopandas",
23
+ "pandas",
24
+ "numpy",
25
+ "scipy",
26
+ "shapely",
27
+ "networkx",
28
+ "pillow",
29
+ "matplotlib",
30
+ "dataclasses",
31
+ "typing",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/DOSull/weaving-space"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ """
5
+ See [this notebook](https://github.com/DOSull/weaving-space/blob/main/using-the-library.ipynb) for some preliminary usage notes.
6
+ """
7
+
8
+ from .tiling_utils import *
9
+ from ._loom import *
10
+ from ._weave_grid import *
11
+ from .tileable import *
12
+ from .tiling_geometries import *
13
+ from .tile_unit import *
14
+ from .weave_matrices import *
15
+ from .weave_unit import *
16
+ from .tile_map import *
17
+ from .symmetry import *
18
+ from .topology_elements import *
19
+ from .topology import *
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ from dataclasses import dataclass
5
+
6
+ import numpy as np
7
+
8
+
9
+ _decode_orders = (
10
+ {1: (0, ), 2: (1, ), 3: ( ), 4: (0, 1), 5: (1, 0)},
11
+ {1: (1, ), 2: (0, ), 3: ( ), 4: (1, 0), 5: (0, 1)},
12
+ {1: (2, ), 2: (1, ), 3: ( ), 4: (2, 1), 5: (1, 2)},
13
+ {1: (0, ), 2: (2, ), 3: ( ), 4: (0, 2), 5: (2, 0)}
14
+ )
15
+ """Initially in a biaxial weave intersection sites are encoded numerically:
16
+
17
+ 1 = warp is absent
18
+ 2 = weft is absent
19
+ 3 = both threads are absent
20
+ 4 = weft is on top
21
+ 5 = warp is on top
22
+
23
+ this tuple of dictionaries translates these to tuples listing the 'layer' order
24
+ depending on the input axis, where item 0 is the biaxial case with layers 0 and
25
+ 1 only, item 1 is layers 0 and 1, item 2 is layers 1 and 2, and item 3 is
26
+ layers 2 and 0.
27
+ """
28
+
29
+ _combined_orderings = {
30
+ ( ): {( ): {( ): None, # All absent
31
+ (2, ): (2, )}, # Only 2 present
32
+ (1, ): {( ): (1, )}, # Only 1 present
33
+ (2, ): {(2, ): (2, )}}, # Only 2 present
34
+ (0, ): {( ): {(0, ): (0, ), # Only 0 present
35
+ ( ): (0, )}, # Only 0 present
36
+ (2, ): {(2, 0): (2, 0), # 0 2 20 --> 2 > 0
37
+ (0, 2): (0, 2)}}, # 0 2 02 --> 0 > 2
38
+ (1, ): {(1, ): {( ): (1, )}, # Only 1 present
39
+ (1, 2): {(2, ): (1, 2)}, # 1 12 2 --> 1 > 2
40
+ (2, 1): {(2, ): (2, 1)}}, # 1 21 2 --> 2 > 1
41
+ (0, 1): {(1, ): {(0, ): (0, 1)}, # 01 1 0 --> 0 > 1
42
+ (1, 2): {(0, 2): (0, 1, 2)}, # 01 12 02 --> 0 > 1 > 2
43
+ (2, 1): {(2, 0): (2, 0, 1), # 01 21 20 --> 2 > 0 > 1
44
+ (0, 2): (0, 2, 1)}}, # 01 21 02 --> 0 > 2 > 1
45
+ (1, 0): {(1, ): {(0, ): (1, 0)}, # 10 1 0 --> 1 > 0
46
+ (1, 2): {(2, 0): (1, 2, 0), # 10 12 20 --> 1 > 2 > 0
47
+ (0, 2): (1, 0, 2)}, # 10 12 02 --> 1 > 0 > 2
48
+ (2, 1): {(2, 0): (2, 1, 0)}} # 10 21 20 --> 2 > 1 > 0
49
+ }
50
+ """Nested dictionary to combine layer orders from 3 biaxial weaves
51
+ into a single consistent order of layers. Inconsistent combinations
52
+ will not return a value. Layers 0 and 1 are present in the first 'tier',
53
+ layers 1 and 2 in the second and 2 and 0 in the third.
54
+ """
55
+
56
+
57
+ @dataclass
58
+ class Loom:
59
+ """A collection of weave matrices and associated attributes.
60
+
61
+ Central to the loom object are the indices and orderings lists. These
62
+ list the grid coordinate pairs and corresponding layer orderings at the
63
+ site indexed by the coordinates. E.g., for a simple plain weave, we have:
64
+
65
+ indices: [(0, 0), (0, 1), (1, 0), (1, 1)]
66
+ orderings: [(0, 1), (1, 0), (1, 0), (0, 1)]
67
+ """
68
+ indices: list[tuple]
69
+ """grid coordinate pairs."""
70
+ orderings: list
71
+ """list of layer orders at site locations index by corresponding times in
72
+ `indices`."""
73
+ dimensions: tuple
74
+ """maximum coordinate values in each direction."""
75
+ orientations: tuple
76
+ """angles (in degrees) which strands on each axis make with the x-axis.
77
+ (0, -90) for 2 axes, (0, 120, 240) for 3."""
78
+ n_axes: int
79
+ """number of axes, 2 or 3."""
80
+
81
+ def __init__(self, *matrices:np.ndarray):
82
+ """Constructor for a Loom. Takes either one or three weave matrices
83
+ as input and initialises the loom based on these.
84
+ """
85
+ if len(matrices) == 1:
86
+ m = matrices[0]
87
+ self.dimensions = m.shape
88
+ self.n_axes = len(self.dimensions)
89
+ # self.parity = None
90
+ self.orientations = (0, -90)
91
+ self.indices = [(i, j) for i in range(m.shape[0])
92
+ for j in range(m.shape[1])]
93
+ self.orderings = [_decode_orders[0][m[ij]] for ij in self.indices]
94
+ else:
95
+ nA, nB, nC = [max(m.shape) for m in matrices]
96
+ self.dimensions = (nA, nB, nC)
97
+ self.n_axes = len(self.dimensions)
98
+ self.orientations = (0, 120, 240)
99
+ all_indices = [(i, j, k) for i in range(nA)
100
+ for j in range(nB)
101
+ for k in range(nC)]
102
+ # parity is used to select coordinate triples in the grid see:
103
+ # Nagy BN 2003. Shortest Paths in Triangular Grids with
104
+ # Neighbourhood Sequences. Journal of Computing and Information
105
+ # Technology 11 (2):111
106
+ parity = (nA + nB + nC - 3) // 2
107
+ self.indices = [x for x in all_indices if sum(x) in (parity, parity + 1)]
108
+ m1, m2, m3 = matrices
109
+ # extend the input 2D grids if needed
110
+ mAB = np.tile(m1, (np.lcm(nA, m1.shape[0]) // m1.shape[0],
111
+ np.lcm(nA, m1.shape[1]) // m1.shape[1]))
112
+ mBC = np.tile(m2, (np.lcm(nB, m2.shape[0]) // m2.shape[0],
113
+ np.lcm(nB, m2.shape[1]) // m2.shape[1]))
114
+ mCA = np.tile(m3, (np.lcm(nC, m3.shape[0]) // m3.shape[0],
115
+ np.lcm(nC, m3.shape[1]) // m3.shape[1]))
116
+ # get the layer orders in each using 2 of the 3 coordinates
117
+ ordAB = [_decode_orders[1][mAB[ij]]
118
+ for ij in [(x[1], x[0]) for x in self.indices]]
119
+ ordBC = [_decode_orders[2][mBC[ij]]
120
+ for ij in [(x[2], x[1]) for x in self.indices]]
121
+ ordCA = [_decode_orders[3][mCA[ij]]
122
+ for ij in [(x[0], x[2]) for x in self.indices]]
123
+ # # combine orders from the three matrices stacked
124
+ self.orderings = [self._combine_orders(abc)
125
+ for abc in zip(ordAB, ordBC, ordCA)]
126
+
127
+
128
+ # convenience wrapper for the combined_orderings dictionary
129
+ # missing values return "NA"
130
+ def _combine_orders(self, orders):
131
+ # print(f"orders: {orders}")
132
+ try:
133
+ result = _combined_orderings[orders[0]][orders[1]][orders[2]]
134
+ except:
135
+ print(f"Unable to determine unique ordering on {orders}")
136
+ return "NA"
137
+ else:
138
+ return result
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ from typing import Union
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+
9
+ import shapely.affinity as affine
10
+ import shapely.geometry as geom
11
+ import shapely.ops
12
+ import shapely.wkt as wkt
13
+
14
+ import weavingspace.tiling_utils as tiling_utils
15
+
16
+ @dataclass
17
+ class WeaveGrid:
18
+ """Class to encapsulate generation of sites where strands intersect.
19
+
20
+ Can generate both triangular and square grid 'cells' as visual
21
+ representation of overlaps of two or three strands at a site.
22
+
23
+ NOTE: there is no 'gridification' or 'cleaning' in the geometry of this
24
+ code - we leave that to steps further down the pipeline, preferring to
25
+ perform this low level geometry with maximum precision. The only limited
26
+ break with this is in get_visible_cell_strands, where strands in higher
27
+ layers are buffered by a small amount to avoid closely aligned polygon
28
+ edges producing slivers etc.
29
+
30
+ Atrributes:
31
+ basis (np.ndarray): matrix to calculate x,y coordinates of a
32
+ site from its (integer) grid coordinates,
33
+ orientations (tuple[float]: orientations of the two or three
34
+ axes either (0, -90) or (0, 120, 240).
35
+ grid_cell (geom.Polygon): the base triangle or square of the grid.
36
+ n_axes (int): the number of axes in the weave, 2 or 3.
37
+ Defaults to 2.
38
+ spacing (float): spacing of the strands. Defaults to 10_000.
39
+ """
40
+ basis: np.ndarray
41
+ orientations: tuple[float]
42
+ grid_cell: geom.Polygon = None
43
+ n_axes: int = None
44
+ spacing: float = 10000
45
+
46
+ def __init__(self, n_axes, orientations, spacing = 10000):
47
+ """Initialises _WeaveGrid.
48
+ """
49
+ self.n_axes = n_axes
50
+ self.orientations = orientations
51
+ self.spacing = spacing
52
+ self.basis = self.setup_basis()
53
+ self.grid_cell = self._make_grid_cell()
54
+
55
+ def setup_basis(self) -> np.ndarray:
56
+ """Sets up the basis of the grid coordinate generation matrix.
57
+
58
+ Returns:
59
+ np.nd.array: self.n_axes x 2 matrix to generate float coordinates
60
+ of cells in Cartesian space from integer grid coordinates of
61
+ cells.
62
+ """
63
+ # angles are perpendicular to the direction of their respective strands
64
+ # so e.g. 2nd strand in 3 axis case is at 120, and perpendicular is 210
65
+ if self.n_axes == 2:
66
+ angles = (np.pi / 2, 0)
67
+ dx = [self.spacing * np.cos(a) for a in angles] # [0, 1]
68
+ dy = [self.spacing * np.sin(a) for a in angles] # [1, 0]
69
+ elif self.n_axes == 3:
70
+ angles = [np.pi / 6 * x for x in range(3, 12, 4)] # [90, 210, 330]
71
+ dx = [self.spacing * 2 / 3 * np.cos(a) for a in angles]
72
+ dy = [self.spacing * 2 / 3 * np.sin(a) for a in angles]
73
+ return np.array(dx + dy).reshape((2, self.n_axes))
74
+
75
+
76
+ def get_coordinates(self, coords: tuple) -> np.ndarray:
77
+ """Uses self.basis to determine Cartesian coordinates of cell centroid
78
+ from grid coordinates.
79
+
80
+ Args:
81
+ coords (tuple[int]): integer coordinate pair of grid location.
82
+
83
+ Returns:
84
+ np.ndarray: float coordinate pair of grid cell centroid.
85
+ """
86
+ return self.basis @ coords
87
+
88
+
89
+ def get_grid_cell_at(self, coords: tuple[int] = None) -> geom.Polygon:
90
+ """Returns grid cell polygon centred on coords.
91
+
92
+ Args:
93
+ coords (tuple[int], optional): _description_. Defaults to None.
94
+
95
+ Returns:
96
+ geom.Polygon: square or triangle centred at the specified
97
+ coordinates.
98
+ """
99
+ if coords is None:
100
+ coords = tuple([0] * self.n_axes)
101
+ if self.grid_cell is None:
102
+ polygon = self._make_grid_cell()
103
+ else:
104
+ polygon = self.grid_cell
105
+ xy = self.get_coordinates(coords)
106
+ polygon = affine.translate(polygon, xy[0], xy[1])
107
+ if self.n_axes == 2 or sum(coords) %2 == 0:
108
+ return polygon
109
+ else:
110
+ return affine.rotate(polygon, 180, origin = polygon.centroid)
111
+
112
+
113
+ def _make_grid_cell(self) -> geom.Polygon:
114
+ r"""Returns base cell polygon for this _WeaveGrid
115
+
116
+ Grid cell is centred at (0, 0) with number of sides dependent
117
+ on self.n_axes. One side of polygon will be horizontal below the
118
+ x-axis symmetric about the y-axis. Radii to corners are either
119
+
120
+ n_axes=2: n_axes=3:
121
+ \ / |
122
+ \/ |
123
+ /\ / \
124
+ / \ / \
125
+
126
+ face to face distance of cell from base vertically to the opposite face
127
+ or corner is (square) L = 2 Rcos(45), or (triangle) L = R + Rcos(60) where R is the radius of the circumcircle.
128
+
129
+ Polygon is generated by finding points equally spaced on the
130
+ circumcircle. Note that the code could easily be altered to return a
131
+ hexagon, if the need arises (this would require a change to the basis
132
+ calculation also).
133
+
134
+ Returns:
135
+ Polygon: square or triangular cell of appropriate size i.e. spacing
136
+ centred at (0, 0).
137
+ """
138
+ n_sides = 4 if self.n_axes == 2 else 3
139
+ if n_sides == 4:
140
+ R = self.spacing / (2 * np.cos(np.pi / n_sides))
141
+ else:
142
+ R = self.spacing / (1 + np.cos(np.pi / n_sides))
143
+ angles = self._get_angles(n_sides)
144
+ corners = [(R * np.cos(a), R * np.sin(a)) for a in angles]
145
+ return geom.Polygon(corners)
146
+
147
+
148
+ def _get_angles(self, n: int = 4) -> list[float]:
149
+ """Returns angles to corners of n sides polygon, assuming one side is horizontal parallel to x-axis.
150
+
151
+ To determine angles start at 6 o'clock (3pi/2) and add (pi/n), then
152
+ subtract a series of n - 1 2pi/n steps. Note we subtract due to the CW
153
+ polygon winding order convention
154
+
155
+ Args:
156
+ n (int, optional): Number of sides. Defaults to 4.
157
+
158
+ Returns:
159
+ list[float]: angles in radians.
160
+ """
161
+ return [(3 * np.pi/2) + (np.pi/n) - (i/n * 2 * np.pi) for i in range(n)]
162
+
163
+
164
+ def _get_grid_cell_slices(self, L:float, W:float = 1,
165
+ n_slices:int = 1) -> list[geom.Polygon]:
166
+ r"""Gets list of rectangular polygons represneting 'slices' across cell.
167
+
168
+ Returns 'slices' across grid cell (i.e. horizontally) centred vertically
169
+ relative to the cell, ie
170
+
171
+ /\
172
+ / \
173
+ +------------------+
174
+ | / \ |
175
+ +------------------+
176
+ | / \ |
177
+ +------------------+
178
+ / \
179
+ /________________\
180
+
181
+ Horizontal extent is L, total width of the strips is W * self.spacing,
182
+ they are 'sliced' horizontally in n_slices slices of equal width.
183
+
184
+ Args:
185
+ L (float, optional): length of slices. Defaults to 0.
186
+ W (float, optional): width of slices relative to grid spacing.
187
+ Defaults to 1.
188
+ n_slices (int, optional): number of slices to divide strands into
189
+ along their length. Defaults to 1.
190
+
191
+ Returns:
192
+ list[geom.Polygon]: _description_
193
+ """
194
+ L = self.spacing if L == 0 else L
195
+ # note that strand width is based on self.spacing, not L because L
196
+ # may be larger if generating slices for aspect < 1 when strands
197
+ # will extend outside grid cell.
198
+ strand_w = self.spacing * W
199
+ slice_w = strand_w / n_slices
200
+ odd_numbers = [x for x in range(1, 2 * n_slices, 2)]
201
+ slice_offsets = [(slice_w * o / 2) - (strand_w / 2) for o in odd_numbers]
202
+ base_slice = geom.Polygon([(-L/2, -slice_w/2), (-L/2, slice_w/2),
203
+ ( L/2, slice_w/2), ( L/2, -slice_w/2)])
204
+ return [affine.translate(base_slice, 0, offset)
205
+ for offset in slice_offsets]
206
+
207
+
208
+ def _get_cell_strands(
209
+ self, width:float = 1.0, coords:tuple[int] = None,
210
+ orientation:int = 0, n_slices:int = 1
211
+ ) -> list[Union[geom.Polygon,geom.MultiPolygon]]:
212
+ """Gets n_slices cells strands with specified total width across the
213
+ grid cell at coords at orientation.
214
+
215
+ Args:
216
+ width (float, optional): total width of strands relative to
217
+ self.spacing. Defaults to 1.0.
218
+ coords (tuple[int], optional): integer grid coordinates of
219
+ cell. Defaults to None.
220
+ orientation (int, optional): orientation of the strands.
221
+ Defaults to 0.
222
+ n_slices (int, optional): number of length-wise slices to cut
223
+ strands into. Defaults to 1.
224
+
225
+ Returns:
226
+ list[Union[geom.Polygon,geom.MultiPolygon]]: polygons representing the
227
+ strands.
228
+ """
229
+ cell = self.get_grid_cell_at(coords)
230
+ # when aspect is <1 strands extend outside cell by some scale factor
231
+ sf = 2 - width if self.n_axes == 2 else (5 - 3 * width) / 2
232
+ expanded_cell = affine.scale(cell, sf, sf, origin = cell.centroid)
233
+ big_l = (sf * self.spacing ## rectangular case is simple
234
+ if self.n_axes == 2 ## triangular less so!
235
+ else sf * self.spacing * 2 / np.sqrt(3) * (3 - width) / 2)
236
+ strands = geom.MultiPolygon(
237
+ self._get_grid_cell_slices(L = big_l, W = width, n_slices = n_slices))
238
+ # we need centre of cell bounding box to shift strands to
239
+ # vertical center of triangular cells. In rectangular case
240
+ # this will be (0, 0).
241
+ cell_offset = cell.envelope.centroid
242
+ strands = affine.translate(strands, cell_offset.x, cell_offset.y)
243
+ strands = geom.MultiPolygon(
244
+ [expanded_cell.intersection(s) for s in strands.geoms])
245
+ strands = affine.rotate(strands, orientation, origin = cell.centroid)
246
+ return [s for s in strands.geoms]
247
+
248
+
249
+ def get_visible_cell_strands(
250
+ self, width:float= 1.0, coords:tuple[int] = None,
251
+ strand_order:tuple[int] = (0, 1, 2), n_slices:tuple[int] = (1, 1, 1)
252
+ ) -> list[Union[geom.Polygon,geom.MultiPolygon]]:
253
+ """Returns visible strands in grid cell based on layer order.
254
+
255
+ Returns visible parts of the strands in a grid cell, given strand width, strand layer order and the number of slices in each direction.
256
+
257
+ Args:
258
+ width (float): total width of strands relative to self.spacing.
259
+ Defaults to 1.0.
260
+ coords (tuple[int], optional): grid cell coordinates. Defaults
261
+ to None.
262
+ strand_order (tuple[int], optional): order of the layers from top,
263
+ at this cell site. Defaults to (0, 1, 2).
264
+ n_slices (tuple[int], optional): number of slices in each layer
265
+ at this cell site. Defaults to (1, 1, 1).
266
+
267
+ Returns:
268
+ list[Union[geom.Polygon,geom.MultiPolygon]]: those polygons that
269
+ will be visible at this site given requested strand order from
270
+ the top.
271
+ """
272
+ all_polys = []
273
+ for order in strand_order[:self.n_axes]:
274
+ next_polys = self._get_cell_strands(width, coords,
275
+ self.orientations[order],
276
+ n_slices[order])
277
+ if all_polys == []:
278
+ all_polys.extend(next_polys)
279
+ mask = shapely.unary_union(next_polys)
280
+ else:
281
+ # buffering the mask cleans up many issues with closely
282
+ # aligned polygon edges in overlayed layers
283
+ all_polys.extend([p.difference(
284
+ mask.buffer(tiling_utils.RESOLUTION, join_style = 2, cap_style = 3))
285
+ for p in next_polys])
286
+ mask = mask.union(shapely.unary_union(next_polys))
287
+ return all_polys
288
+
289
+
290
+ def get_tile_from_cells(self, approx_tile:geom.Polygon) -> geom.Polygon:
291
+ """Returns a rectangle or hexagon derived from the bounds of the
292
+ supplied approximation to a tile.
293
+
294
+ This is required because we know the required tile is an exact 4 or
295
+ 6 cornered polygon, but the MultiPolygon formed by unary_union is likely
296
+ to have many more corners than this (for some reason...).
297
+
298
+ Args:
299
+ approx_tile (geom.Polygon): MultiPolygon formed from the cells of
300
+ the tile.
301
+
302
+ Returns:
303
+ geom.Polygon (geom.Polygon): rectangle or hexagon geom.Polygon.
304
+ """
305
+ xmin, ymin, xmax, ymax = approx_tile.bounds
306
+ if self.n_axes == 2:
307
+ w = np.round((xmax - xmin) / self.spacing) * self.spacing
308
+ else:
309
+ h_spacing = self.spacing * 2 / np.sqrt(3)
310
+ w = np.round((xmax - xmin) / h_spacing) * h_spacing
311
+ h = np.round((ymax - ymin) / self.spacing) * self.spacing
312
+ if self.n_axes == 2:
313
+ return geom.Polygon([(-w/2, -h/2), (-w/2, h/2),
314
+ (w/2, h/2), (w/2, -h/2)])
315
+ else:
316
+ return geom.Polygon([(w/4, -h/2), (-w/4, -h/2), (-w/2, 0),
317
+ (-w/4, h/2), ( w/4, h/2), (w/2, 0)])
318
+
319
+
320
+ # THIS IS HERE FOR SAFE KEEPING... at various times the shapely.set_precision
321
+ # function causes havoc in this class, and the below has proven more stable
322
+ # Note that the more aggressive precision setting here might be relevant also
323
+ # def _gridify(self, shape: geom.Polygon, precision = 4) -> geom.Polygon:
324
+ # """Returns polygon with coordinates at specified precision.
325
+
326
+ # IMPORTANT (and not understood...!!!). This local method seems to work
327
+ # better on the kinds of gridification required for the weave grid polygons
328
+ # to work... I have no idea why. It may relate to the reordering of
329
+ # coordinates that shapely.set_precision engages in... or... it might not.
330
+ # It clearly has something to do with floating point issues. I JUST DON'T
331
+ # KNOW...
332
+
333
+ # TODO: Figure this the hell out at some point!
334
+
335
+ # Args:
336
+ # shape (geom.Polygon): polygon to gridify.
337
+ # precision (int): digits of precision. Defaults to 4.
338
+
339
+ # Returns:
340
+ # geom.Polygon: gridified polygon.
341
+ # """
342
+ # return wkt.loads(wkt.dumps(shape, rounding_precision = precision))
343
+