yirgacheffe 1.8.1__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.

Files changed (60) hide show
  1. {yirgacheffe-1.8.1/yirgacheffe.egg-info → yirgacheffe-1.9.0}/PKG-INFO +7 -1
  2. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/README.md +5 -0
  3. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/pyproject.toml +2 -1
  4. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_base.py +61 -26
  5. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_h3layer.py +1 -1
  6. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_openers.py +1 -3
  7. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_operators.py +81 -0
  8. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_pickle.py +1 -1
  9. yirgacheffe-1.9.0/tests/test_projection.py +45 -0
  10. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_vectors.py +2 -4
  11. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_operators.py +44 -2
  12. yirgacheffe-1.9.0/yirgacheffe/constants.py +8 -0
  13. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/base.py +39 -12
  14. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/window.py +20 -7
  15. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0/yirgacheffe.egg-info}/PKG-INFO +7 -1
  16. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/requires.txt +1 -0
  17. yirgacheffe-1.8.1/tests/test_projection.py +0 -32
  18. yirgacheffe-1.8.1/yirgacheffe/constants.py +0 -8
  19. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/LICENSE +0 -0
  20. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/MANIFEST.in +0 -0
  21. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/setup.cfg +0 -0
  22. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_area.py +0 -0
  23. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_auto_windowing.py +0 -0
  24. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_constants.py +0 -0
  25. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_datatypes.py +0 -0
  26. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_group.py +0 -0
  27. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_intersection.py +0 -0
  28. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_multiband.py +0 -0
  29. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_nodata.py +0 -0
  30. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_optimisation.py +0 -0
  31. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_parallel_operators.py +0 -0
  32. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_raster.py +0 -0
  33. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_rescaling.py +0 -0
  34. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_rounding.py +0 -0
  35. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_save_with_window.py +0 -0
  36. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_sum_with_window.py +0 -0
  37. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_uniform_area_layer.py +0 -0
  38. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_union.py +0 -0
  39. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/tests/test_window.py +0 -0
  40. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/__init__.py +0 -0
  41. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_backends/__init__.py +0 -0
  42. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_backends/enumeration.py +0 -0
  43. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_backends/mlx.py +0 -0
  44. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_backends/numpy.py +0 -0
  45. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/_core.py +0 -0
  46. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/__init__.py +0 -0
  47. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/area.py +0 -0
  48. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/constant.py +0 -0
  49. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/group.py +0 -0
  50. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/h3layer.py +0 -0
  51. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/rasters.py +0 -0
  52. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/rescaled.py +0 -0
  53. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/layers/vectors.py +0 -0
  54. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/operators.py +0 -0
  55. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/py.typed +0 -0
  56. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe/rounding.py +0 -0
  57. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/SOURCES.txt +0 -0
  58. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  59. {yirgacheffe-1.8.1 → yirgacheffe-1.9.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
  60. {yirgacheffe-1.8.1 → 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.8.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
+ [![CI](https://github.com/quantifyearth/yirgacheffe/actions/workflows/pull-request.yml/badge.svg?branch=main)](https://github.com/quantifyearth/yirgacheffe/actions)
49
+ [![Documentation](https://img.shields.io/badge/docs-yirgacheffe.org-blue)](https://yirgacheffe.org)
50
+ [![PyPI version](https://img.shields.io/pypi/v/yirgacheffe)](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
+ [![CI](https://github.com/quantifyearth/yirgacheffe/actions/workflows/pull-request.yml/badge.svg?branch=main)](https://github.com/quantifyearth/yirgacheffe/actions)
4
+ [![Documentation](https://img.shields.io/badge/docs-yirgacheffe.org-blue)](https://yirgacheffe.org)
5
+ [![PyPI version](https://img.shields.io/pypi/v/yirgacheffe)](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.8.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
 
@@ -1,136 +1,166 @@
1
1
  import math
2
+ import os
3
+ import tempfile
2
4
 
3
5
  import pytest
4
6
 
5
- from yirgacheffe import WGS_84_PROJECTION
7
+ import yirgacheffe as yg
6
8
  from yirgacheffe.layers import YirgacheffeLayer
7
9
  from yirgacheffe.window import Area, MapProjection
8
10
 
9
- def test_pixel_to_latlng_unsupported_projection() -> None:
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 test_pixel_from_latlng_unsupported_projection() -> None:
18
- layer = YirgacheffeLayer(
19
- Area(-10, 10, 10, -10),
20
- MapProjection("OTHER PROJECTION", 0.02, -0.02),
21
- )
22
- with pytest.raises(NotImplementedError):
23
- _ = layer.pixel_for_latlng(10.0, 10.0)
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(area: Area, pixel: tuple[int, int], expected: tuple[float, float]) -> None:
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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(area: Area, coord: tuple[float, float], expected: tuple[int, int]) -> None:
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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.projection == WGS_84_PROJECTION
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
@@ -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.projection == WGS_84_PROJECTION
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.projection == WGS_84_PROJECTION
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)
@@ -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.projection == WGS_84_PROJECTION
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.projection == WGS_84_PROJECTION
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:
@@ -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, x_coord: int, y_coord: int) -> tuple[float, float]:
336
- """Get geo coords for pixel. This is relative to the set view window."""
337
- if self._projection is None or "WGS 84" not in self._projection.name:
338
- raise NotImplementedError("Not yet supported for other projections")
339
- return (
340
- (y_coord * self._projection.ystep) + self.area.top,
341
- (x_coord * self._projection.xstep) + self.area.left
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
- if self._projection is None or "WGS 84" not in self._projection.name:
348
- raise NotImplementedError("Not yet supported for other projections")
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((lng - self.area.left) / self._projection.xstep, abs(self._projection.xstep)),
351
- round_down_pixels((lat - self.area.top) / self._projection.ystep, abs(self._projection.ystep)),
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
  )
@@ -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
- name : str
27
- xstep : float
28
- ystep : float
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.name == other.name) and \
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.8.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
+ [![CI](https://github.com/quantifyearth/yirgacheffe/actions/workflows/pull-request.yml/badge.svg?branch=main)](https://github.com/quantifyearth/yirgacheffe/actions)
49
+ [![Documentation](https://img.shields.io/badge/docs-yirgacheffe.org-blue)](https://yirgacheffe.org)
50
+ [![PyPI version](https://img.shields.io/pypi/v/yirgacheffe)](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.
@@ -6,6 +6,7 @@ dill
6
6
  deprecation
7
7
  tomli
8
8
  h3
9
+ pyproj
9
10
 
10
11
  [dev]
11
12
  mypy
@@ -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