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.
- weavingspace-0.0.1/LICENSE +19 -0
- weavingspace-0.0.1/PKG-INFO +58 -0
- weavingspace-0.0.1/README.md +14 -0
- weavingspace-0.0.1/pyproject.toml +35 -0
- weavingspace-0.0.1/setup.cfg +4 -0
- weavingspace-0.0.1/src/__init__.py +19 -0
- weavingspace-0.0.1/src/_loom.py +138 -0
- weavingspace-0.0.1/src/_weave_grid.py +343 -0
- weavingspace-0.0.1/src/symmetry.py +603 -0
- weavingspace-0.0.1/src/tile_map.py +947 -0
- weavingspace-0.0.1/src/tile_unit.py +253 -0
- weavingspace-0.0.1/src/tileable.py +572 -0
- weavingspace-0.0.1/src/tiling_geometries.py +686 -0
- weavingspace-0.0.1/src/tiling_utils.py +1076 -0
- weavingspace-0.0.1/src/topology.py +1123 -0
- weavingspace-0.0.1/src/topology_elements.py +585 -0
- weavingspace-0.0.1/src/weave_matrices.py +346 -0
- weavingspace-0.0.1/src/weave_unit.py +403 -0
- weavingspace-0.0.1/src/weavingspace.egg-info/PKG-INFO +58 -0
- weavingspace-0.0.1/src/weavingspace.egg-info/SOURCES.txt +21 -0
- weavingspace-0.0.1/src/weavingspace.egg-info/dependency_links.txt +1 -0
- weavingspace-0.0.1/src/weavingspace.egg-info/requires.txt +14 -0
- weavingspace-0.0.1/src/weavingspace.egg-info/top_level.txt +13 -0
|
@@ -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,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
|
+
|