yirgacheffe 1.8.0__tar.gz → 1.9.0__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 yirgacheffe might be problematic. Click here for more details.
- {yirgacheffe-1.8.0/yirgacheffe.egg-info → yirgacheffe-1.9.0}/PKG-INFO +7 -1
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/README.md +5 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/pyproject.toml +2 -1
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_area.py +3 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_auto_windowing.py +2 -2
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_base.py +61 -26
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_h3layer.py +1 -1
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_intersection.py +30 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_openers.py +1 -3
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_operators.py +81 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_pickle.py +1 -1
- yirgacheffe-1.9.0/tests/test_projection.py +45 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_union.py +11 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_vectors.py +2 -4
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/__init__.py +1 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_backends/enumeration.py +20 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_operators.py +44 -2
- yirgacheffe-1.9.0/yirgacheffe/constants.py +8 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/base.py +39 -12
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/vectors.py +29 -29
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/window.py +23 -8
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0/yirgacheffe.egg-info}/PKG-INFO +7 -1
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/requires.txt +1 -0
- yirgacheffe-1.8.0/tests/test_projection.py +0 -32
- yirgacheffe-1.8.0/yirgacheffe/constants.py +0 -8
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/LICENSE +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/MANIFEST.in +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/setup.cfg +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_constants.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_datatypes.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_group.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_multiband.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_nodata.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_parallel_operators.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_raster.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/tests/test_window.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/_core.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/area.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/constant.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/group.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/h3layer.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/rasters.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/layers/rescaled.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/operators.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/py.typed +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/SOURCES.txt +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.8.0 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Abstraction of gdal datasets for doing basic math operations
|
|
5
5
|
Author-email: Michael Dales <mwd24@cam.ac.uk>
|
|
6
6
|
License-Expression: ISC
|
|
@@ -27,6 +27,7 @@ Requires-Dist: dill
|
|
|
27
27
|
Requires-Dist: deprecation
|
|
28
28
|
Requires-Dist: tomli
|
|
29
29
|
Requires-Dist: h3
|
|
30
|
+
Requires-Dist: pyproj
|
|
30
31
|
Provides-Extra: mlx
|
|
31
32
|
Requires-Dist: mlx; extra == "mlx"
|
|
32
33
|
Provides-Extra: dev
|
|
@@ -44,6 +45,11 @@ Dynamic: license-file
|
|
|
44
45
|
|
|
45
46
|
# Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
|
|
46
47
|
|
|
48
|
+
[](https://github.com/quantifyearth/yirgacheffe/actions)
|
|
49
|
+
[](https://yirgacheffe.org)
|
|
50
|
+
[](https://pypi.org/project/yirgacheffe/)
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
## Overview
|
|
48
54
|
|
|
49
55
|
Yirgacheffe is an attempt to wrap raster and polygon geospatial datasets such that you can do computational work on them as a whole or at the pixel level, but without having to do a lot of the grunt work of working out where you need to be in rasters, or managing how much you can load into memory safely.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
|
|
2
2
|
|
|
3
|
+
[](https://github.com/quantifyearth/yirgacheffe/actions)
|
|
4
|
+
[](https://yirgacheffe.org)
|
|
5
|
+
[](https://pypi.org/project/yirgacheffe/)
|
|
6
|
+
|
|
7
|
+
|
|
3
8
|
## Overview
|
|
4
9
|
|
|
5
10
|
Yirgacheffe is an attempt to wrap raster and polygon geospatial datasets such that you can do computational work on them as a whole or at the pixel level, but without having to do a lot of the grunt work of working out where you need to be in rasters, or managing how much you can load into memory safely.
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "yirgacheffe"
|
|
9
|
-
version = "1.
|
|
9
|
+
version = "1.9.0"
|
|
10
10
|
description = "Abstraction of gdal datasets for doing basic math operations"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [{ name = "Michael Dales", email = "mwd24@cam.ac.uk" }]
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
"deprecation",
|
|
31
31
|
"tomli",
|
|
32
32
|
"h3",
|
|
33
|
+
"pyproj",
|
|
33
34
|
]
|
|
34
35
|
requires-python = ">=3.10"
|
|
35
36
|
|
|
@@ -4,7 +4,7 @@ import tempfile
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
import yirgacheffe
|
|
7
|
+
import yirgacheffe as yg
|
|
8
8
|
from tests.helpers import gdal_dataset_with_data, make_vectors_with_mutlile_ids
|
|
9
9
|
from yirgacheffe.layers import ConstantLayer, RasterLayer, VectorLayer
|
|
10
10
|
from yirgacheffe.window import Area
|
|
@@ -315,7 +315,7 @@ def test_vector_layers_multiply() -> None:
|
|
|
315
315
|
expected = np.array([[2, 0], [0, 8]])
|
|
316
316
|
assert (expected == actual).all()
|
|
317
317
|
|
|
318
|
-
@pytest.mark.skipif(
|
|
318
|
+
@pytest.mark.skipif(yg._backends.BACKEND != "NUMPY", reason="Only applies for numpy")
|
|
319
319
|
def test_parallel_save_windows() -> None:
|
|
320
320
|
data1 = np.array([[1, 2], [3, 4]])
|
|
321
321
|
data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80], [90, 100, 110, 120], [130, 140, 150, 160]])
|
|
@@ -1,136 +1,166 @@
|
|
|
1
1
|
import math
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
2
4
|
|
|
3
5
|
import pytest
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import yirgacheffe as yg
|
|
6
8
|
from yirgacheffe.layers import YirgacheffeLayer
|
|
7
9
|
from yirgacheffe.window import Area, MapProjection
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
layer = YirgacheffeLayer(
|
|
11
|
-
Area(-10, 10, 10, -10),
|
|
12
|
-
MapProjection("OTHER PROJECTION", 0.02, -0.02),
|
|
13
|
-
)
|
|
14
|
-
with pytest.raises(NotImplementedError):
|
|
15
|
-
_ = layer.latlng_for_pixel(10, 10)
|
|
11
|
+
from tests.helpers import make_vectors_with_id
|
|
16
12
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
def test_pixel_to_latlng_no_projection() -> None:
|
|
14
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
15
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
16
|
+
area = Area(-10.0, 10.0, 10.0, 0.0)
|
|
17
|
+
make_vectors_with_id(42, {area}, path)
|
|
18
|
+
with yg.read_shape(path) as layer:
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
_ = layer.latlng_for_pixel(10, 10)
|
|
21
|
+
|
|
22
|
+
def test_latlng_to_pixel_no_projection() -> None:
|
|
23
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
24
|
+
path = os.path.join(tempdir, "test.gpkg")
|
|
25
|
+
area = Area(-10.0, 10.0, 10.0, 0.0)
|
|
26
|
+
make_vectors_with_id(42, {area}, path)
|
|
27
|
+
with yg.read_shape(path) as layer:
|
|
28
|
+
with pytest.raises(ValueError):
|
|
29
|
+
_ = layer.pixel_for_latlng(10.0, 10.0)
|
|
24
30
|
|
|
25
31
|
@pytest.mark.parametrize(
|
|
26
|
-
"area,pixel,expected",
|
|
32
|
+
"area,projection,pixel,expected",
|
|
27
33
|
[
|
|
28
34
|
(
|
|
29
35
|
Area(-10, 10, 10, -10),
|
|
36
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
30
37
|
(0, 0),
|
|
31
38
|
(10.0, -10.0)
|
|
32
39
|
),
|
|
33
40
|
(
|
|
34
41
|
Area(-10, 10, 10, -10),
|
|
42
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
35
43
|
(1, 1),
|
|
36
44
|
(9.8, -9.8)
|
|
37
45
|
),
|
|
38
46
|
(
|
|
39
47
|
Area(-10, 10, 10, -10),
|
|
48
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
40
49
|
(101, 101),
|
|
41
50
|
(-10.2, 10.2)
|
|
42
51
|
),
|
|
43
52
|
(
|
|
44
53
|
Area(-10, 10, 10, -10),
|
|
54
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
45
55
|
(-1, -1),
|
|
46
56
|
(10.2, -10.2)
|
|
47
57
|
),
|
|
48
58
|
(
|
|
49
59
|
Area(10, 10, 20, -10),
|
|
60
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
50
61
|
(1, 1),
|
|
51
62
|
(9.8, 10.2)
|
|
52
63
|
),
|
|
53
64
|
(
|
|
54
65
|
Area(-10, -10, 10, -20),
|
|
66
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
55
67
|
(1, 1),
|
|
56
68
|
(-10.2, -9.8)
|
|
57
69
|
),
|
|
58
70
|
]
|
|
59
71
|
)
|
|
60
|
-
def test_latlng_for_pixel(
|
|
72
|
+
def test_latlng_for_pixel(
|
|
73
|
+
area: Area,
|
|
74
|
+
projection: MapProjection,
|
|
75
|
+
pixel: tuple[int, int],
|
|
76
|
+
expected: tuple[float, float]
|
|
77
|
+
) -> None:
|
|
61
78
|
layer = YirgacheffeLayer(
|
|
62
79
|
area,
|
|
63
|
-
|
|
80
|
+
projection,
|
|
64
81
|
)
|
|
65
82
|
result = layer.latlng_for_pixel(*pixel)
|
|
66
83
|
assert math.isclose(result[0], expected[0])
|
|
67
84
|
assert math.isclose(result[1], expected[1])
|
|
68
85
|
|
|
69
86
|
@pytest.mark.parametrize(
|
|
70
|
-
"area,coord,expected",
|
|
87
|
+
"area,projection,coord,expected",
|
|
71
88
|
[
|
|
72
89
|
(
|
|
73
90
|
Area(-10, 10, 10, -10),
|
|
91
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
74
92
|
(10.0, -10.0),
|
|
75
93
|
(0, 0)
|
|
76
94
|
),
|
|
77
95
|
(
|
|
78
96
|
Area(-10, 10, 10, -10),
|
|
97
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
79
98
|
(9.8, -9.8),
|
|
80
99
|
(1, 1)
|
|
81
100
|
),
|
|
82
101
|
(
|
|
83
102
|
Area(-10, 10, 10, -10),
|
|
103
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
84
104
|
(0.0, 0.0),
|
|
85
105
|
(50, 50)
|
|
86
106
|
),
|
|
87
107
|
]
|
|
88
108
|
)
|
|
89
|
-
def test_pixel_for_latlng(
|
|
109
|
+
def test_pixel_for_latlng(
|
|
110
|
+
area: Area,
|
|
111
|
+
projection: MapProjection,
|
|
112
|
+
coord: tuple[float, float],
|
|
113
|
+
expected: tuple[int, int]
|
|
114
|
+
) -> None:
|
|
90
115
|
layer = YirgacheffeLayer(
|
|
91
116
|
area,
|
|
92
|
-
|
|
117
|
+
projection,
|
|
93
118
|
)
|
|
94
119
|
result = layer.pixel_for_latlng(*coord)
|
|
95
120
|
assert result == expected
|
|
96
121
|
|
|
97
|
-
|
|
98
122
|
@pytest.mark.parametrize(
|
|
99
|
-
"area,window,pixel,expected",
|
|
123
|
+
"area,window,projection,pixel,expected",
|
|
100
124
|
[
|
|
101
125
|
(
|
|
102
126
|
Area(-10, 10, 10, -10),
|
|
103
127
|
Area(-5, 5, 5, -5),
|
|
128
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
104
129
|
(0, 0),
|
|
105
130
|
(5.0, -5.0)
|
|
106
131
|
),
|
|
107
132
|
(
|
|
108
133
|
Area(-10, 10, 10, -10),
|
|
109
134
|
Area(-5, 5, 5, -5),
|
|
135
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
110
136
|
(1, 1),
|
|
111
137
|
(4.8, -4.8)
|
|
112
138
|
),
|
|
113
139
|
(
|
|
114
140
|
Area(-10, 10, 10, -10),
|
|
115
141
|
Area(-5, 5, 5, -5),
|
|
142
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
116
143
|
(101, 101),
|
|
117
144
|
(-15.2, 15.2)
|
|
118
145
|
),
|
|
119
146
|
(
|
|
120
147
|
Area(-10, 10, 10, -10),
|
|
121
148
|
Area(-5, 5, 5, -5),
|
|
149
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
122
150
|
(-1, -1),
|
|
123
151
|
(5.2, -5.2)
|
|
124
152
|
),
|
|
125
153
|
(
|
|
126
154
|
Area(10, 10, 20, -10),
|
|
127
155
|
Area(15, 5, 20, -5),
|
|
156
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
128
157
|
(1, 1),
|
|
129
158
|
(4.8, 15.2)
|
|
130
159
|
),
|
|
131
160
|
(
|
|
132
161
|
Area(-10, -10, 10, -20),
|
|
133
162
|
Area(-5, -15, 5, -20),
|
|
163
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
134
164
|
(1, 1),
|
|
135
165
|
(-15.2, -4.8)
|
|
136
166
|
),
|
|
@@ -139,12 +169,13 @@ def test_pixel_for_latlng(area: Area, coord: tuple[float, float], expected: tupl
|
|
|
139
169
|
def test_latlng_for_pixel_with_intersection(
|
|
140
170
|
area: Area,
|
|
141
171
|
window: Area,
|
|
172
|
+
projection: MapProjection,
|
|
142
173
|
pixel: tuple[int, int],
|
|
143
174
|
expected: tuple[float, float]
|
|
144
175
|
) -> None:
|
|
145
176
|
layer = YirgacheffeLayer(
|
|
146
177
|
area,
|
|
147
|
-
|
|
178
|
+
projection,
|
|
148
179
|
)
|
|
149
180
|
layer.set_window_for_intersection(window)
|
|
150
181
|
result = layer.latlng_for_pixel(*pixel)
|
|
@@ -152,23 +183,26 @@ def test_latlng_for_pixel_with_intersection(
|
|
|
152
183
|
assert math.isclose(result[1], expected[1])
|
|
153
184
|
|
|
154
185
|
@pytest.mark.parametrize(
|
|
155
|
-
"area,window,coord,expected",
|
|
186
|
+
"area,window,projection,coord,expected",
|
|
156
187
|
[
|
|
157
188
|
(
|
|
158
189
|
Area(-10, 10, 10, -10),
|
|
159
190
|
Area(-5, 5, 5, -5),
|
|
191
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
160
192
|
(5.0, -5.0),
|
|
161
193
|
(0, 0)
|
|
162
194
|
),
|
|
163
195
|
(
|
|
164
196
|
Area(-10, 10, 10, -10),
|
|
165
197
|
Area(-5, 5, 5, -5),
|
|
198
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
166
199
|
(4.8, -4.8),
|
|
167
200
|
(1, 1)
|
|
168
201
|
),
|
|
169
202
|
(
|
|
170
203
|
Area(-10, 10, 10, -10),
|
|
171
204
|
Area(-5, 5, 5, -5),
|
|
205
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
172
206
|
(0.0, 0.0),
|
|
173
207
|
(25, 25)
|
|
174
208
|
),
|
|
@@ -177,12 +211,13 @@ def test_latlng_for_pixel_with_intersection(
|
|
|
177
211
|
def test_pixel_for_latlng_with_intersection(
|
|
178
212
|
area: Area,
|
|
179
213
|
window: Area,
|
|
214
|
+
projection: MapProjection,
|
|
180
215
|
coord: tuple[float, float],
|
|
181
216
|
expected: tuple[int, int]
|
|
182
217
|
) -> None:
|
|
183
218
|
layer = YirgacheffeLayer(
|
|
184
219
|
area,
|
|
185
|
-
|
|
220
|
+
projection,
|
|
186
221
|
)
|
|
187
222
|
layer.set_window_for_intersection(window)
|
|
188
223
|
result = layer.pixel_for_latlng(*coord)
|
|
@@ -23,7 +23,7 @@ def test_h3_layer(cell_id: str, is_valid: bool, expected_zoom: int) -> None:
|
|
|
23
23
|
if is_valid:
|
|
24
24
|
with H3CellLayer(cell_id, MapProjection(WGS_84_PROJECTION, 0.001, -0.001)) as layer:
|
|
25
25
|
assert layer.zoom == expected_zoom
|
|
26
|
-
assert layer.
|
|
26
|
+
assert layer.map_projection.epsg == 4326
|
|
27
27
|
|
|
28
28
|
# without getting too deep, we'd expect a mix of zeros and ones in the data
|
|
29
29
|
window = layer.window
|
|
@@ -59,6 +59,9 @@ def test_find_intersection_with_constant() -> None:
|
|
|
59
59
|
intersection = RasterLayer.find_intersection(layers)
|
|
60
60
|
assert intersection == layers[0].area
|
|
61
61
|
|
|
62
|
+
for layer in layers:
|
|
63
|
+
layer.set_window_for_intersection(intersection)
|
|
64
|
+
|
|
62
65
|
def test_find_intersection_with_vector_unbound() -> None:
|
|
63
66
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
64
67
|
path = Path(tempdir) / "test.gpkg"
|
|
@@ -74,6 +77,10 @@ def test_find_intersection_with_vector_unbound() -> None:
|
|
|
74
77
|
intersection = RasterLayer.find_intersection(layers)
|
|
75
78
|
assert intersection == vector.area
|
|
76
79
|
|
|
80
|
+
raster.set_window_for_intersection(intersection)
|
|
81
|
+
with pytest.raises(ValueError):
|
|
82
|
+
vector.set_window_for_intersection(intersection)
|
|
83
|
+
|
|
77
84
|
def test_find_intersection_with_vector_bound() -> None:
|
|
78
85
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
79
86
|
path = Path(tempdir) / "test.gpkg"
|
|
@@ -89,6 +96,29 @@ def test_find_intersection_with_vector_bound() -> None:
|
|
|
89
96
|
intersection = RasterLayer.find_intersection(layers)
|
|
90
97
|
assert intersection == vector.area
|
|
91
98
|
|
|
99
|
+
for layer in layers:
|
|
100
|
+
layer.set_window_for_intersection(intersection)
|
|
101
|
+
|
|
102
|
+
def test_find_intersection_with_vector_awkward_rounding() -> None:
|
|
103
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
104
|
+
path = Path(tempdir) / "test.gpkg"
|
|
105
|
+
area = Area(left=-90, top=45, right=90, bottom=-45)
|
|
106
|
+
make_vectors_with_id(42, {area}, path)
|
|
107
|
+
assert path.exists
|
|
108
|
+
|
|
109
|
+
raster = RasterLayer(gdal_dataset_of_region(Area(left=-180, top=90, right=180, bottom=-90), 18.0))
|
|
110
|
+
vector = VectorLayer.layer_from_file(path, None, raster.map_projection.scale, raster.map_projection.name)
|
|
111
|
+
|
|
112
|
+
rounded_area = Area(left=-90, top=54, right=90, bottom=-54)
|
|
113
|
+
assert vector.area == rounded_area
|
|
114
|
+
|
|
115
|
+
layers = [raster, vector]
|
|
116
|
+
intersection = RasterLayer.find_intersection(layers)
|
|
117
|
+
assert intersection == vector.area
|
|
118
|
+
|
|
119
|
+
for layer in layers:
|
|
120
|
+
layer.set_window_for_intersection(intersection)
|
|
121
|
+
|
|
92
122
|
def test_find_intersection_different_pixel_pitch() -> None:
|
|
93
123
|
layers = [
|
|
94
124
|
RasterLayer(gdal_dataset_of_region(Area(-10, 10, 10, -10), 0.02)),
|
|
@@ -90,7 +90,6 @@ def test_open_gpkg() -> None:
|
|
|
90
90
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
91
91
|
assert layer.window == Window(0, 0, 20, 10)
|
|
92
92
|
assert layer.map_projection == MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
|
|
93
|
-
assert layer.projection == WGS_84_PROJECTION
|
|
94
93
|
|
|
95
94
|
def test_open_gpkg_with_mapprojection() -> None:
|
|
96
95
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -103,7 +102,6 @@ def test_open_gpkg_with_mapprojection() -> None:
|
|
|
103
102
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
104
103
|
assert layer.window == Window(0, 0, 20, 10)
|
|
105
104
|
assert layer.map_projection == MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
|
|
106
|
-
assert layer.projection == WGS_84_PROJECTION
|
|
107
105
|
|
|
108
106
|
def test_open_gpkg_with_no_projection() -> None:
|
|
109
107
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -129,7 +127,7 @@ def test_open_gpkg_direct_scale() -> None:
|
|
|
129
127
|
assert layer.area == area
|
|
130
128
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
131
129
|
assert layer.window == Window(0, 0, 20, 10)
|
|
132
|
-
assert layer.
|
|
130
|
+
assert layer.map_projection == MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
|
|
133
131
|
|
|
134
132
|
def test_open_gpkg_with_filter() -> None:
|
|
135
133
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -1596,3 +1596,84 @@ def test_isnan() -> None:
|
|
|
1596
1596
|
actual = result.read_array(0, 0, 4, 2)
|
|
1597
1597
|
expected = data1 == 5.0
|
|
1598
1598
|
assert (expected == actual).all()
|
|
1599
|
+
|
|
1600
|
+
@pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
|
|
1601
|
+
def test_add_byte_layers_read_array_all(monkeypatch, blocksize) -> None:
|
|
1602
|
+
with monkeypatch.context() as m:
|
|
1603
|
+
m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
|
|
1604
|
+
data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
|
|
1605
|
+
data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
|
|
1606
|
+
|
|
1607
|
+
with (
|
|
1608
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
|
|
1609
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
|
|
1610
|
+
):
|
|
1611
|
+
comp = layer1 + layer2
|
|
1612
|
+
expected = data1 + data2
|
|
1613
|
+
actual = comp.read_array(0, 0, 4, 2)
|
|
1614
|
+
assert (expected == actual).all()
|
|
1615
|
+
|
|
1616
|
+
@pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
|
|
1617
|
+
def test_add_byte_layers_read_array_partial_horizontal(monkeypatch, blocksize) -> None:
|
|
1618
|
+
with monkeypatch.context() as m:
|
|
1619
|
+
m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
|
|
1620
|
+
data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
|
|
1621
|
+
data2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80]])
|
|
1622
|
+
|
|
1623
|
+
with (
|
|
1624
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
|
|
1625
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
|
|
1626
|
+
):
|
|
1627
|
+
comp = layer1 + layer2
|
|
1628
|
+
expected = (data1 + data2)[0:3,1:3]
|
|
1629
|
+
actual = comp.read_array(1, 0, 2, 2)
|
|
1630
|
+
assert (expected == actual).all()
|
|
1631
|
+
|
|
1632
|
+
@pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
|
|
1633
|
+
def test_add_byte_layers_read_array_partial_vertical(monkeypatch, blocksize) -> None:
|
|
1634
|
+
with monkeypatch.context() as m:
|
|
1635
|
+
m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
|
|
1636
|
+
data1 = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
|
|
1637
|
+
data2 = np.array([[10, 20], [30, 40], [50, 60], [70, 80]])
|
|
1638
|
+
|
|
1639
|
+
with (
|
|
1640
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
|
|
1641
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
|
|
1642
|
+
):
|
|
1643
|
+
comp = layer1 + layer2
|
|
1644
|
+
expected = (data1 + data2)[1:3,0:3]
|
|
1645
|
+
actual = comp.read_array(0, 1, 2, 2)
|
|
1646
|
+
assert (expected == actual).all()
|
|
1647
|
+
|
|
1648
|
+
@pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
|
|
1649
|
+
def test_add_byte_layers_read_array_partial(monkeypatch, blocksize) -> None:
|
|
1650
|
+
with monkeypatch.context() as m:
|
|
1651
|
+
m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
|
|
1652
|
+
data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
|
|
1653
|
+
data2 = np.array([[10, 10, 10, 10], [20, 20, 20, 20], [30, 30, 30, 30], [40, 40, 40, 40]])
|
|
1654
|
+
|
|
1655
|
+
with (
|
|
1656
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
|
|
1657
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
|
|
1658
|
+
):
|
|
1659
|
+
comp = layer1 + layer2
|
|
1660
|
+
expected = (data1 + data2)[1:3,1:3]
|
|
1661
|
+
actual = comp.read_array(1, 1, 2, 2)
|
|
1662
|
+
assert (expected == actual).all()
|
|
1663
|
+
|
|
1664
|
+
@pytest.mark.parametrize("blocksize", [1, 2, 4, 8])
|
|
1665
|
+
def test_add_byte_layers_read_array_superset(monkeypatch, blocksize) -> None:
|
|
1666
|
+
with monkeypatch.context() as m:
|
|
1667
|
+
m.setattr(yirgacheffe.constants, "YSTEP", blocksize)
|
|
1668
|
+
data1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
|
|
1669
|
+
data2 = np.array([[10, 10, 10, 10], [20, 20, 20, 20], [30, 30, 30, 30], [40, 40, 40, 40]])
|
|
1670
|
+
|
|
1671
|
+
with (
|
|
1672
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data1)) as layer1,
|
|
1673
|
+
RasterLayer(gdal_dataset_with_data((0.0, 0.0), 0.02, data2)) as layer2,
|
|
1674
|
+
):
|
|
1675
|
+
comp = layer1 + layer2
|
|
1676
|
+
inner_expected = data1 + data2
|
|
1677
|
+
expected = np.pad(inner_expected, (1, 1))
|
|
1678
|
+
actual = comp.read_array(-1, -1, 6, 6)
|
|
1679
|
+
assert (expected == actual).all()
|
|
@@ -48,7 +48,7 @@ def test_pickle_dyanamic_vector_layer() -> None:
|
|
|
48
48
|
assert restore.area == area
|
|
49
49
|
assert restore.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
50
50
|
assert restore.window == Window(0, 0, 20, 10)
|
|
51
|
-
assert restore.
|
|
51
|
+
assert restore.map_projection == MapProjection(WGS_84_PROJECTION, 1.0, -1.0)
|
|
52
52
|
|
|
53
53
|
del layer
|
|
54
54
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pyproj
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from yirgacheffe.window import MapProjection
|
|
5
|
+
from yirgacheffe.rounding import MINIMAL_DEGREE_OF_INTEREST
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize("name", [
|
|
8
|
+
"epsg:4326",
|
|
9
|
+
"esri:54009",
|
|
10
|
+
pyproj.CRS.from_string("epsg:4326").to_wkt(),
|
|
11
|
+
pyproj.CRS.from_string("esri:54009").to_wkt(),
|
|
12
|
+
])
|
|
13
|
+
def test_scale_from_projection(name) -> None:
|
|
14
|
+
projection = MapProjection(name, 0.1, -0.1)
|
|
15
|
+
assert projection.name == pyproj.CRS.from_string(name).to_wkt()
|
|
16
|
+
assert projection.xstep == 0.1
|
|
17
|
+
assert projection.ystep == -0.1
|
|
18
|
+
scale = projection.scale
|
|
19
|
+
assert scale.xstep == 0.1
|
|
20
|
+
assert scale.ystep == -0.1
|
|
21
|
+
|
|
22
|
+
PROJ_A = "epsg:4326"
|
|
23
|
+
PROJ_B = "esri:54009"
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"lhs,rhs,is_equal",
|
|
27
|
+
[
|
|
28
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1, -0.1), True),
|
|
29
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_B, 0.1, -0.1), False),
|
|
30
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1, 0.1), False),
|
|
31
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, -0.1, 0.1), False),
|
|
32
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1 + (MINIMAL_DEGREE_OF_INTEREST / 2), -0.1), True),
|
|
33
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1 - (MINIMAL_DEGREE_OF_INTEREST / 2), -0.1), True),
|
|
34
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1, -0.1 + (MINIMAL_DEGREE_OF_INTEREST / 2)), True),
|
|
35
|
+
(MapProjection(PROJ_A, 0.1, -0.1), MapProjection(PROJ_A, 0.1, -0.1 - (MINIMAL_DEGREE_OF_INTEREST / 2)), True),
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
def test_projection_equality(lhs: MapProjection, rhs : MapProjection, is_equal: bool) -> None:
|
|
39
|
+
assert MINIMAL_DEGREE_OF_INTEREST > 0.0
|
|
40
|
+
assert (lhs == rhs) == is_equal
|
|
41
|
+
assert (lhs != rhs) == (not is_equal)
|
|
42
|
+
|
|
43
|
+
def test_invalid_projection_name() -> None:
|
|
44
|
+
with pytest.raises(ValueError):
|
|
45
|
+
_ = MapProjection("random name", 1.0, -1.0)
|
|
@@ -49,6 +49,9 @@ def test_find_union_distinct() -> None:
|
|
|
49
49
|
union = RasterLayer.find_union(layers)
|
|
50
50
|
assert union == Area(-110, 10, 110, -10)
|
|
51
51
|
|
|
52
|
+
for layer in layers:
|
|
53
|
+
layer.set_window_for_union(union)
|
|
54
|
+
|
|
52
55
|
def test_find_union_with_null() -> None:
|
|
53
56
|
layers = [
|
|
54
57
|
RasterLayer(gdal_dataset_of_region(Area(-10, 10, 10, -10), 0.02)),
|
|
@@ -80,6 +83,11 @@ def test_find_union_with_vector_unbound() -> None:
|
|
|
80
83
|
union = RasterLayer.find_union(layers)
|
|
81
84
|
assert union == vector.area
|
|
82
85
|
|
|
86
|
+
raster.set_window_for_union(union)
|
|
87
|
+
with pytest.raises(ValueError):
|
|
88
|
+
vector.set_window_for_union(union)
|
|
89
|
+
|
|
90
|
+
|
|
83
91
|
def test_find_union_with_vector_bound() -> None:
|
|
84
92
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
85
93
|
path = Path(tempdir) / "test.gpkg"
|
|
@@ -95,6 +103,9 @@ def test_find_union_with_vector_bound() -> None:
|
|
|
95
103
|
union = RasterLayer.find_union(layers)
|
|
96
104
|
assert union == vector.area
|
|
97
105
|
|
|
106
|
+
for layer in layers:
|
|
107
|
+
layer.set_window_for_union(union)
|
|
108
|
+
|
|
98
109
|
@pytest.mark.parametrize("scale", [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09])
|
|
99
110
|
def test_set_union_self(scale) -> None:
|
|
100
111
|
layer = RasterLayer(gdal_dataset_of_region(Area(-10, 10, 10, -10), scale))
|
|
@@ -29,8 +29,7 @@ def test_basic_dynamic_vector_layer() -> None:
|
|
|
29
29
|
assert layer.area == area
|
|
30
30
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
31
31
|
assert layer.window == Window(0, 0, 20, 10)
|
|
32
|
-
assert layer.
|
|
33
|
-
assert layer.map_projection.name == WGS_84_PROJECTION
|
|
32
|
+
assert layer.map_projection.epsg == 4326
|
|
34
33
|
|
|
35
34
|
# The astype here is to catch escaping MLX types...
|
|
36
35
|
res = layer.read_array(0, 0, 20, 20).astype(int)
|
|
@@ -46,8 +45,7 @@ def test_rastered_vector_layer() -> None:
|
|
|
46
45
|
assert layer.area == area
|
|
47
46
|
assert layer.geo_transform == (area.left, 1.0, 0.0, area.top, 0.0, -1.0)
|
|
48
47
|
assert layer.window == Window(0, 0, 20, 10)
|
|
49
|
-
assert layer.
|
|
50
|
-
assert layer.map_projection.name == WGS_84_PROJECTION
|
|
48
|
+
assert layer.map_projection.epsg == 4326
|
|
51
49
|
|
|
52
50
|
def test_basic_dynamic_vector_layer_no_filter_match() -> None:
|
|
53
51
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
@@ -15,5 +15,6 @@ except ModuleNotFoundError:
|
|
|
15
15
|
from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
|
|
16
16
|
from .constants import WGS_84_PROJECTION
|
|
17
17
|
from .window import Area, MapProjection, Window
|
|
18
|
+
from ._backends.enumeration import dtype as DataType
|
|
18
19
|
|
|
19
20
|
gdal.UseExceptions()
|
|
@@ -41,6 +41,26 @@ class operators(Enum):
|
|
|
41
41
|
ISNAN = 36
|
|
42
42
|
|
|
43
43
|
class dtype(Enum):
|
|
44
|
+
"""Represents the type of data returned by a layer.
|
|
45
|
+
|
|
46
|
+
This enumeration defines the valid data types supported by Yirgacheffe, and is
|
|
47
|
+
what is returned by calling `datatype` on a layer or expression, and can be
|
|
48
|
+
passed to `astype` to convert values between types.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
Float32: 32 bit floating point value
|
|
52
|
+
Float64: 64 bit floating point value
|
|
53
|
+
Byte: Unsigned 8 bit integer value
|
|
54
|
+
Int8: Signed 8 bit integer value
|
|
55
|
+
Int16: Signed 16 bit integer value
|
|
56
|
+
Int32: Signed 32 bit integer value
|
|
57
|
+
Int64: Signed 64 bit integer value
|
|
58
|
+
UInt8: Unsigned 8 bit integer value
|
|
59
|
+
UInt16: Unsigned 16 bit integer value
|
|
60
|
+
UInt32: Unsigned 32 bit integer value
|
|
61
|
+
UInt64: Unsigned 64 bit integer value
|
|
62
|
+
"""
|
|
63
|
+
|
|
44
64
|
Float32 = gdal.GDT_Float32
|
|
45
65
|
Float64 = gdal.GDT_Float64
|
|
46
66
|
Byte = gdal.GDT_Byte
|
|
@@ -654,8 +654,8 @@ class LayerOperation(LayerMathMixin):
|
|
|
654
654
|
for yoffset in range(0, computation_window.ysize, self.ystep):
|
|
655
655
|
if callback:
|
|
656
656
|
callback(yoffset / computation_window.ysize)
|
|
657
|
-
step=self.ystep
|
|
658
|
-
if yoffset+step > computation_window.ysize:
|
|
657
|
+
step = self.ystep
|
|
658
|
+
if yoffset + step > computation_window.ysize:
|
|
659
659
|
step = computation_window.ysize - yoffset
|
|
660
660
|
chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
|
|
661
661
|
if isinstance(chunk, (float, int)):
|
|
@@ -935,6 +935,48 @@ class LayerOperation(LayerMathMixin):
|
|
|
935
935
|
|
|
936
936
|
return result
|
|
937
937
|
|
|
938
|
+
def read_array(self, x: int, y: int, width: int, height: int) -> np.ndarray:
|
|
939
|
+
"""Read an area of pixles from the specified area of a calculated raster.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
x: X axis offset for reading
|
|
943
|
+
y: Y axis offset for reading
|
|
944
|
+
width: Width of data to read
|
|
945
|
+
height: Height of data to read
|
|
946
|
+
|
|
947
|
+
Returns:
|
|
948
|
+
A numpy array containing the requested data. If the region of data read goes
|
|
949
|
+
beyond the bounds of the calculation that area will be filled with zeros.
|
|
950
|
+
"""
|
|
951
|
+
projection = self.map_projection
|
|
952
|
+
if projection is None:
|
|
953
|
+
raise ValueError("No map projection specified for layers in expression")
|
|
954
|
+
|
|
955
|
+
computation_window = Window(0, 0, width, height)
|
|
956
|
+
expression_area = self.area
|
|
957
|
+
pixel_scale = projection.scale
|
|
958
|
+
left = expression_area.left + (x * pixel_scale.xstep)
|
|
959
|
+
top = expression_area.top + (y * pixel_scale.ystep)
|
|
960
|
+
computation_area = Area(
|
|
961
|
+
left=left,
|
|
962
|
+
top=top,
|
|
963
|
+
right=left + (width * pixel_scale.xstep),
|
|
964
|
+
bottom=top + (height * pixel_scale.ystep),
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
chunks = []
|
|
968
|
+
for yoffset in range(0, height, self.ystep):
|
|
969
|
+
step = self.ystep
|
|
970
|
+
if yoffset + step > height:
|
|
971
|
+
step = height - yoffset
|
|
972
|
+
chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
|
|
973
|
+
if isinstance(chunk, (float, int)):
|
|
974
|
+
chunk = backend.full((step, computation_window.xsize), chunk)
|
|
975
|
+
chunks.append(chunk)
|
|
976
|
+
res = np.vstack(chunks)
|
|
977
|
+
|
|
978
|
+
return res
|
|
979
|
+
|
|
938
980
|
class ShaderStyleOperation(LayerOperation):
|
|
939
981
|
|
|
940
982
|
def _eval(self, area, projection, index, step, target_window=None):
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pyproj
|
|
2
|
+
|
|
3
|
+
YSTEP = 512
|
|
4
|
+
MINIMUM_CHUNKS_PER_THREAD = 1
|
|
5
|
+
|
|
6
|
+
# I don't really want this here, but it's just too useful having it exposed
|
|
7
|
+
# This used to be a fixed string, but now it is at least programmatically generated
|
|
8
|
+
WGS_84_PROJECTION = pyproj.CRS.from_epsg(4326).to_wkt(version='WKT1_GDAL')
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
from typing import Any, Sequence
|
|
3
3
|
|
|
4
4
|
import deprecation
|
|
5
|
+
from pyproj import Transformer
|
|
5
6
|
|
|
6
7
|
from .. import __version__
|
|
7
8
|
from .._operators import LayerMathMixin
|
|
@@ -332,21 +333,47 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
332
333
|
res = self._read_array(x, y, width, height)
|
|
333
334
|
return backend.demote_array(res)
|
|
334
335
|
|
|
335
|
-
def latlng_for_pixel(self,
|
|
336
|
-
"""Get geo coords for pixel. This is relative to the set view window.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
336
|
+
def latlng_for_pixel(self, x: int, y: int) -> tuple[float, float]:
|
|
337
|
+
"""Get geo coords for pixel. This is relative to the set view window.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
x: X axis position within raster
|
|
341
|
+
y: Y axis position within raster
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
A tuple containing the (latitude, longitude).
|
|
345
|
+
"""
|
|
346
|
+
projection = self.map_projection
|
|
347
|
+
if projection is None:
|
|
348
|
+
raise ValueError("Map has not projection space")
|
|
349
|
+
pixel_scale = projection.scale
|
|
350
|
+
coord_in_raster_space = (
|
|
351
|
+
(y * pixel_scale.ystep) + self.area.top,
|
|
352
|
+
(x * pixel_scale.xstep) + self.area.left,
|
|
342
353
|
)
|
|
354
|
+
transformer = Transformer.from_crs(projection.name, "EPSG:4326")
|
|
355
|
+
return transformer.transform(*coord_in_raster_space)
|
|
343
356
|
|
|
344
357
|
def pixel_for_latlng(self, lat: float, lng: float) -> tuple[int, int]:
|
|
345
358
|
"""Get pixel for geo coords. This is relative to the set view window.
|
|
346
|
-
Result is rounded down to nearest pixel.
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
Result is rounded down to nearest pixel.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
lat: Geospatial latitude in WGS84
|
|
363
|
+
lng: Geospatial longitude in WGS84
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A tuple containing the x, y coordinates in pixel space.
|
|
367
|
+
"""
|
|
368
|
+
projection = self.map_projection
|
|
369
|
+
if projection is None:
|
|
370
|
+
raise ValueError("Map has not projection space")
|
|
371
|
+
|
|
372
|
+
transformer = Transformer.from_crs("EPSG:4326", projection.name)
|
|
373
|
+
x, y = transformer.transform(lng,lat)
|
|
374
|
+
|
|
375
|
+
pixel_scale = projection.scale
|
|
349
376
|
return (
|
|
350
|
-
round_down_pixels((
|
|
351
|
-
round_down_pixels((
|
|
377
|
+
round_down_pixels((x - self.area.left) / pixel_scale.xstep, abs(pixel_scale.xstep)),
|
|
378
|
+
round_down_pixels((y - self.area.top) / pixel_scale.ystep, abs(pixel_scale.ystep)),
|
|
352
379
|
)
|
|
@@ -333,36 +333,36 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
333
333
|
self._anchor = anchor
|
|
334
334
|
self._envelopes = envelopes
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
336
|
+
if projection is not None:
|
|
337
|
+
# Get the area, but scale it to the pixel resolution that we're using. Note that
|
|
338
|
+
# the pixel scale GDAL uses can have -ve values, but those will mess up the
|
|
339
|
+
# ceil/floor math, so we use absolute versions when trying to round.
|
|
340
|
+
abs_xstep, abs_ystep = abs(projection.xstep), abs(projection.ystep)
|
|
341
|
+
|
|
342
|
+
# Lacking any other reference, we will make the raster align with
|
|
343
|
+
# (0.0, 0.0), if sometimes we want to align with an existing raster, so if
|
|
344
|
+
# an anchor is specified, ensure we use that as our pixel space alignment
|
|
345
|
+
x_anchor = anchor[0]
|
|
346
|
+
y_anchor = anchor[1]
|
|
347
|
+
left_shift = x_anchor - abs_xstep
|
|
348
|
+
right_shift = x_anchor
|
|
349
|
+
top_shift = y_anchor
|
|
350
|
+
bottom_shift = y_anchor - abs_ystep
|
|
351
|
+
|
|
352
|
+
area = Area(
|
|
353
|
+
left=(floor((min(x[0] for x in envelopes) - left_shift) / abs_xstep) * abs_xstep) + left_shift,
|
|
354
|
+
top=(ceil((max(x[3] for x in envelopes) - top_shift) / abs_ystep) * abs_ystep) + top_shift,
|
|
355
|
+
right=(ceil((max(x[1] for x in envelopes) - right_shift) / abs_xstep) * abs_xstep) + right_shift,
|
|
356
|
+
bottom=(floor((min(x[2] for x in envelopes) - bottom_shift) / abs_ystep) * abs_ystep) + bottom_shift,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
359
|
# If we don't have a projection just go with the idealised area
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
area = Area(
|
|
361
|
+
left=floor(min(x[0] for x in envelopes)),
|
|
362
|
+
top=ceil(max(x[3] for x in envelopes)),
|
|
363
|
+
right=ceil(max(x[1] for x in envelopes)),
|
|
364
|
+
bottom=floor(min(x[2] for x in envelopes)),
|
|
365
|
+
)
|
|
366
366
|
|
|
367
367
|
super().__init__(area, projection)
|
|
368
368
|
|
|
@@ -4,37 +4,50 @@ import sys
|
|
|
4
4
|
from collections import namedtuple
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
+
import pyproj
|
|
8
|
+
|
|
7
9
|
PixelScale = namedtuple('PixelScale', ['xstep', 'ystep'])
|
|
8
10
|
|
|
9
|
-
@dataclass
|
|
10
11
|
class MapProjection:
|
|
11
12
|
"""Records the map projection and the size of the pixels in a layer.
|
|
12
13
|
|
|
13
14
|
This superceeeds the old PixelScale class, which will be removed in version 2.0.
|
|
14
15
|
|
|
15
16
|
Args:
|
|
16
|
-
name: The map projection used.
|
|
17
|
+
name: The map projection used in WKT format, or as "epsg:xxxx" or "esri:xxxx".
|
|
17
18
|
xstep: The number of units horizontal distance a step of one pixel makes in the map projection.
|
|
18
19
|
ystep: The number of units vertical distance a step of one pixel makes in the map projection.
|
|
19
20
|
|
|
20
21
|
Attributes:
|
|
21
|
-
name: The map projection used.
|
|
22
|
+
name: The map projection used in WKT format.
|
|
22
23
|
xstep: The number of units horizontal distance a step of one pixel makes in the map projection.
|
|
23
24
|
ystep: The number of units vertical distance a step of one pixel makes in the map projection.
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
def __init__(self, projection_string: str, xstep: float, ystep: float) -> None:
|
|
28
|
+
try:
|
|
29
|
+
self.crs = pyproj.CRS.from_string(projection_string)
|
|
30
|
+
except pyproj.exceptions.CRSError as exc:
|
|
31
|
+
raise ValueError(f"Invalid projection: {projection_string}") from exc
|
|
32
|
+
self.xstep = xstep
|
|
33
|
+
self.ystep = ystep
|
|
29
34
|
|
|
30
35
|
def __eq__(self, other) -> bool:
|
|
31
36
|
if other is None:
|
|
32
37
|
return True
|
|
33
38
|
# to avoid circular dependancies
|
|
34
39
|
from .rounding import are_pixel_scales_equal_enough # pylint: disable=C0415
|
|
35
|
-
return (self.
|
|
40
|
+
return (self.crs == other.crs) and \
|
|
36
41
|
are_pixel_scales_equal_enough([self.scale, other.scale])
|
|
37
42
|
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
return self.crs.to_wkt()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def epsg(self) -> int | None:
|
|
49
|
+
return self.crs.to_epsg()
|
|
50
|
+
|
|
38
51
|
@property
|
|
39
52
|
def scale(self) -> PixelScale:
|
|
40
53
|
return PixelScale(self.xstep, self.ystep)
|
|
@@ -72,7 +85,9 @@ class Area:
|
|
|
72
85
|
def __hash__(self):
|
|
73
86
|
return (self.left, self.top, self.right, self.bottom).__hash__()
|
|
74
87
|
|
|
75
|
-
def __eq__(self, other) -> bool:
|
|
88
|
+
def __eq__(self, other: object) -> bool:
|
|
89
|
+
if not isinstance(other, Area):
|
|
90
|
+
return False
|
|
76
91
|
return math.isclose(self.left, other.left, abs_tol=1e-09) and \
|
|
77
92
|
math.isclose(self.right, other.right, abs_tol=1e-09) and \
|
|
78
93
|
math.isclose(self.top, other.top, abs_tol=1e-09) and \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Abstraction of gdal datasets for doing basic math operations
|
|
5
5
|
Author-email: Michael Dales <mwd24@cam.ac.uk>
|
|
6
6
|
License-Expression: ISC
|
|
@@ -27,6 +27,7 @@ Requires-Dist: dill
|
|
|
27
27
|
Requires-Dist: deprecation
|
|
28
28
|
Requires-Dist: tomli
|
|
29
29
|
Requires-Dist: h3
|
|
30
|
+
Requires-Dist: pyproj
|
|
30
31
|
Provides-Extra: mlx
|
|
31
32
|
Requires-Dist: mlx; extra == "mlx"
|
|
32
33
|
Provides-Extra: dev
|
|
@@ -44,6 +45,11 @@ Dynamic: license-file
|
|
|
44
45
|
|
|
45
46
|
# Yirgacheffe: a declarative geospatial library for Python to make data-science with maps easier
|
|
46
47
|
|
|
48
|
+
[](https://github.com/quantifyearth/yirgacheffe/actions)
|
|
49
|
+
[](https://yirgacheffe.org)
|
|
50
|
+
[](https://pypi.org/project/yirgacheffe/)
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
## Overview
|
|
48
54
|
|
|
49
55
|
Yirgacheffe is an attempt to wrap raster and polygon geospatial datasets such that you can do computational work on them as a whole or at the pixel level, but without having to do a lot of the grunt work of working out where you need to be in rasters, or managing how much you can load into memory safely.
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from yirgacheffe.window import MapProjection
|
|
4
|
-
from yirgacheffe.rounding import MINIMAL_DEGREE_OF_INTEREST
|
|
5
|
-
|
|
6
|
-
def test_scale_from_projection() -> None:
|
|
7
|
-
projection = MapProjection("PROJ", 0.1, -0.1)
|
|
8
|
-
assert projection.name == "PROJ"
|
|
9
|
-
assert projection.xstep == 0.1
|
|
10
|
-
assert projection.ystep == -0.1
|
|
11
|
-
|
|
12
|
-
scale = projection.scale
|
|
13
|
-
assert scale.xstep == 0.1
|
|
14
|
-
assert scale.ystep == -0.1
|
|
15
|
-
|
|
16
|
-
@pytest.mark.parametrize(
|
|
17
|
-
"lhs,rhs,is_equal",
|
|
18
|
-
[
|
|
19
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1, -0.1), True),
|
|
20
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("B", 0.1, -0.1), False),
|
|
21
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1, 0.1), False),
|
|
22
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", -0.1, 0.1), False),
|
|
23
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1 + (MINIMAL_DEGREE_OF_INTEREST / 2), -0.1), True),
|
|
24
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1 - (MINIMAL_DEGREE_OF_INTEREST / 2), -0.1), True),
|
|
25
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1, -0.1 + (MINIMAL_DEGREE_OF_INTEREST / 2)), True),
|
|
26
|
-
(MapProjection("A", 0.1, -0.1), MapProjection("A", 0.1, -0.1 - (MINIMAL_DEGREE_OF_INTEREST / 2)), True),
|
|
27
|
-
]
|
|
28
|
-
)
|
|
29
|
-
def test_projection_equality(lhs: MapProjection, rhs : MapProjection, is_equal: bool) -> None:
|
|
30
|
-
assert MINIMAL_DEGREE_OF_INTEREST > 0.0
|
|
31
|
-
assert (lhs == rhs) == is_equal
|
|
32
|
-
assert (lhs != rhs) == (not is_equal)
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
YSTEP = 512
|
|
2
|
-
MINIMUM_CHUNKS_PER_THREAD = 1
|
|
3
|
-
|
|
4
|
-
# I don't really want this here, but it's just too useful having it exposed
|
|
5
|
-
WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
|
|
6
|
-
'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
|
|
7
|
-
'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
|
|
8
|
-
'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|