yirgacheffe 1.8.1__tar.gz → 1.9.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 yirgacheffe might be problematic. Click here for more details.
- {yirgacheffe-1.8.1/yirgacheffe.egg-info → yirgacheffe-1.9.1}/PKG-INFO +7 -1
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/README.md +5 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/pyproject.toml +2 -1
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_h3layer.py +1 -1
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_openers.py +1 -3
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_operators.py +81 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_pickle.py +1 -1
- yirgacheffe-1.8.1/tests/test_base.py → yirgacheffe-1.9.1/tests/test_pixel_coord.py +90 -25
- yirgacheffe-1.9.1/tests/test_projection.py +45 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_vectors.py +2 -4
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_operators.py +93 -2
- yirgacheffe-1.9.1/yirgacheffe/constants.py +8 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/base.py +0 -19
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/window.py +20 -7
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1/yirgacheffe.egg-info}/PKG-INFO +7 -1
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/SOURCES.txt +1 -1
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/requires.txt +1 -0
- yirgacheffe-1.8.1/tests/test_projection.py +0 -32
- yirgacheffe-1.8.1/yirgacheffe/constants.py +0 -8
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/LICENSE +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/MANIFEST.in +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/setup.cfg +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_area.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_auto_windowing.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_constants.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_datatypes.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_group.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_intersection.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_multiband.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_nodata.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_optimisation.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_parallel_operators.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_raster.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_rescaling.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_rounding.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_save_with_window.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_sum_with_window.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_uniform_area_layer.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_union.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_window.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/__init__.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/__init__.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/enumeration.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/mlx.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/numpy.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_core.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/__init__.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/area.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/constant.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/group.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/h3layer.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/rasters.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/rescaled.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/vectors.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/operators.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/py.typed +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/rounding.py +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/dependency_links.txt +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/entry_points.txt +0 -0
- {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/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.1
|
|
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.1"
|
|
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
|
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -1,136 +1,196 @@
|
|
|
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
|
|
|
86
|
+
def test_latlng_for_pixel_on_operator() -> None:
|
|
87
|
+
layer1 = YirgacheffeLayer(
|
|
88
|
+
Area(-10, 10, 10, -10),
|
|
89
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
90
|
+
)
|
|
91
|
+
layer2 = YirgacheffeLayer(
|
|
92
|
+
Area(-10, 10, 10, -10),
|
|
93
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
94
|
+
)
|
|
95
|
+
calc = layer1 + layer2
|
|
96
|
+
result = calc.latlng_for_pixel(0, 0)
|
|
97
|
+
expected = (10.0, -10.0)
|
|
98
|
+
assert math.isclose(result[0], expected[0])
|
|
99
|
+
assert math.isclose(result[1], expected[1])
|
|
100
|
+
|
|
69
101
|
@pytest.mark.parametrize(
|
|
70
|
-
"area,coord,expected",
|
|
102
|
+
"area,projection,coord,expected",
|
|
71
103
|
[
|
|
72
104
|
(
|
|
73
105
|
Area(-10, 10, 10, -10),
|
|
106
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
74
107
|
(10.0, -10.0),
|
|
75
108
|
(0, 0)
|
|
76
109
|
),
|
|
77
110
|
(
|
|
78
111
|
Area(-10, 10, 10, -10),
|
|
112
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
79
113
|
(9.8, -9.8),
|
|
80
114
|
(1, 1)
|
|
81
115
|
),
|
|
82
116
|
(
|
|
83
117
|
Area(-10, 10, 10, -10),
|
|
118
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
84
119
|
(0.0, 0.0),
|
|
85
120
|
(50, 50)
|
|
86
121
|
),
|
|
87
122
|
]
|
|
88
123
|
)
|
|
89
|
-
def test_pixel_for_latlng(
|
|
124
|
+
def test_pixel_for_latlng(
|
|
125
|
+
area: Area,
|
|
126
|
+
projection: MapProjection,
|
|
127
|
+
coord: tuple[float, float],
|
|
128
|
+
expected: tuple[int, int]
|
|
129
|
+
) -> None:
|
|
90
130
|
layer = YirgacheffeLayer(
|
|
91
131
|
area,
|
|
92
|
-
|
|
132
|
+
projection,
|
|
93
133
|
)
|
|
94
134
|
result = layer.pixel_for_latlng(*coord)
|
|
95
135
|
assert result == expected
|
|
96
136
|
|
|
137
|
+
def test_pixel_for_latlng_on_operator() -> None:
|
|
138
|
+
layer1 = YirgacheffeLayer(
|
|
139
|
+
Area(-10, 10, 10, -10),
|
|
140
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
141
|
+
)
|
|
142
|
+
layer2 = YirgacheffeLayer(
|
|
143
|
+
Area(-10, 10, 10, -10),
|
|
144
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
145
|
+
)
|
|
146
|
+
calc = layer1 + layer2
|
|
147
|
+
result = calc.pixel_for_latlng(10, -10)
|
|
148
|
+
expected = (0, 0)
|
|
149
|
+
assert math.isclose(result[0], expected[0])
|
|
150
|
+
assert math.isclose(result[1], expected[1])
|
|
97
151
|
|
|
98
152
|
@pytest.mark.parametrize(
|
|
99
|
-
"area,window,pixel,expected",
|
|
153
|
+
"area,window,projection,pixel,expected",
|
|
100
154
|
[
|
|
101
155
|
(
|
|
102
156
|
Area(-10, 10, 10, -10),
|
|
103
157
|
Area(-5, 5, 5, -5),
|
|
158
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
104
159
|
(0, 0),
|
|
105
160
|
(5.0, -5.0)
|
|
106
161
|
),
|
|
107
162
|
(
|
|
108
163
|
Area(-10, 10, 10, -10),
|
|
109
164
|
Area(-5, 5, 5, -5),
|
|
165
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
110
166
|
(1, 1),
|
|
111
167
|
(4.8, -4.8)
|
|
112
168
|
),
|
|
113
169
|
(
|
|
114
170
|
Area(-10, 10, 10, -10),
|
|
115
171
|
Area(-5, 5, 5, -5),
|
|
172
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
116
173
|
(101, 101),
|
|
117
174
|
(-15.2, 15.2)
|
|
118
175
|
),
|
|
119
176
|
(
|
|
120
177
|
Area(-10, 10, 10, -10),
|
|
121
178
|
Area(-5, 5, 5, -5),
|
|
179
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
122
180
|
(-1, -1),
|
|
123
181
|
(5.2, -5.2)
|
|
124
182
|
),
|
|
125
183
|
(
|
|
126
184
|
Area(10, 10, 20, -10),
|
|
127
185
|
Area(15, 5, 20, -5),
|
|
186
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
128
187
|
(1, 1),
|
|
129
188
|
(4.8, 15.2)
|
|
130
189
|
),
|
|
131
190
|
(
|
|
132
191
|
Area(-10, -10, 10, -20),
|
|
133
192
|
Area(-5, -15, 5, -20),
|
|
193
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
134
194
|
(1, 1),
|
|
135
195
|
(-15.2, -4.8)
|
|
136
196
|
),
|
|
@@ -139,12 +199,13 @@ def test_pixel_for_latlng(area: Area, coord: tuple[float, float], expected: tupl
|
|
|
139
199
|
def test_latlng_for_pixel_with_intersection(
|
|
140
200
|
area: Area,
|
|
141
201
|
window: Area,
|
|
202
|
+
projection: MapProjection,
|
|
142
203
|
pixel: tuple[int, int],
|
|
143
204
|
expected: tuple[float, float]
|
|
144
205
|
) -> None:
|
|
145
206
|
layer = YirgacheffeLayer(
|
|
146
207
|
area,
|
|
147
|
-
|
|
208
|
+
projection,
|
|
148
209
|
)
|
|
149
210
|
layer.set_window_for_intersection(window)
|
|
150
211
|
result = layer.latlng_for_pixel(*pixel)
|
|
@@ -152,23 +213,26 @@ def test_latlng_for_pixel_with_intersection(
|
|
|
152
213
|
assert math.isclose(result[1], expected[1])
|
|
153
214
|
|
|
154
215
|
@pytest.mark.parametrize(
|
|
155
|
-
"area,window,coord,expected",
|
|
216
|
+
"area,window,projection,coord,expected",
|
|
156
217
|
[
|
|
157
218
|
(
|
|
158
219
|
Area(-10, 10, 10, -10),
|
|
159
220
|
Area(-5, 5, 5, -5),
|
|
221
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
160
222
|
(5.0, -5.0),
|
|
161
223
|
(0, 0)
|
|
162
224
|
),
|
|
163
225
|
(
|
|
164
226
|
Area(-10, 10, 10, -10),
|
|
165
227
|
Area(-5, 5, 5, -5),
|
|
228
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
166
229
|
(4.8, -4.8),
|
|
167
230
|
(1, 1)
|
|
168
231
|
),
|
|
169
232
|
(
|
|
170
233
|
Area(-10, 10, 10, -10),
|
|
171
234
|
Area(-5, 5, 5, -5),
|
|
235
|
+
MapProjection("epsg:4326", 0.2, -0.2),
|
|
172
236
|
(0.0, 0.0),
|
|
173
237
|
(25, 25)
|
|
174
238
|
),
|
|
@@ -177,12 +241,13 @@ def test_latlng_for_pixel_with_intersection(
|
|
|
177
241
|
def test_pixel_for_latlng_with_intersection(
|
|
178
242
|
area: Area,
|
|
179
243
|
window: Area,
|
|
244
|
+
projection: MapProjection,
|
|
180
245
|
coord: tuple[float, float],
|
|
181
246
|
expected: tuple[int, int]
|
|
182
247
|
) -> None:
|
|
183
248
|
layer = YirgacheffeLayer(
|
|
184
249
|
area,
|
|
185
|
-
|
|
250
|
+
projection,
|
|
186
251
|
)
|
|
187
252
|
layer.set_window_for_intersection(window)
|
|
188
253
|
result = layer.pixel_for_latlng(*coord)
|
|
@@ -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.
|
|
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:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
3
4
|
import logging
|
|
4
5
|
import math
|
|
5
6
|
import multiprocessing
|
|
@@ -21,6 +22,7 @@ import numpy as np
|
|
|
21
22
|
import numpy.typing as npt
|
|
22
23
|
from osgeo import gdal
|
|
23
24
|
from dill import dumps, loads # type: ignore
|
|
25
|
+
from pyproj import Transformer
|
|
24
26
|
|
|
25
27
|
from . import constants, __version__
|
|
26
28
|
from .rounding import round_up_pixels, round_down_pixels
|
|
@@ -290,6 +292,53 @@ class LayerMathMixin:
|
|
|
290
292
|
datatype=datatype
|
|
291
293
|
)
|
|
292
294
|
|
|
295
|
+
def latlng_for_pixel(self, x: int, y: int) -> tuple[float, float]:
|
|
296
|
+
"""Get geo coords for pixel. This is relative to the set view window.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
x: X axis position within raster
|
|
300
|
+
y: Y axis position within raster
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
A tuple containing the (latitude, longitude).
|
|
304
|
+
"""
|
|
305
|
+
projection = self.map_projection # type: ignore[attr-defined]
|
|
306
|
+
area = self.area # type: ignore[attr-defined]
|
|
307
|
+
if projection is None:
|
|
308
|
+
raise ValueError("Map has not projection space")
|
|
309
|
+
pixel_scale = projection.scale
|
|
310
|
+
coord_in_raster_space = (
|
|
311
|
+
(y * pixel_scale.ystep) + area.top,
|
|
312
|
+
(x * pixel_scale.xstep) + area.left,
|
|
313
|
+
)
|
|
314
|
+
transformer = Transformer.from_crs(projection.name, "EPSG:4326")
|
|
315
|
+
return transformer.transform(*coord_in_raster_space)
|
|
316
|
+
|
|
317
|
+
def pixel_for_latlng(self, lat: float, lng: float) -> tuple[int, int]:
|
|
318
|
+
"""Get pixel for geo coords. This is relative to the set view window.
|
|
319
|
+
Result is rounded down to nearest pixel.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
lat: Geospatial latitude in WGS84
|
|
323
|
+
lng: Geospatial longitude in WGS84
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
A tuple containing the x, y coordinates in pixel space.
|
|
327
|
+
"""
|
|
328
|
+
projection = self.map_projection # type: ignore[attr-defined]
|
|
329
|
+
area = self.area # type: ignore[attr-defined]
|
|
330
|
+
if projection is None:
|
|
331
|
+
raise ValueError("Map has not projection space")
|
|
332
|
+
|
|
333
|
+
transformer = Transformer.from_crs("EPSG:4326", projection.name)
|
|
334
|
+
x, y = transformer.transform(lng,lat)
|
|
335
|
+
|
|
336
|
+
pixel_scale = projection.scale
|
|
337
|
+
return (
|
|
338
|
+
round_down_pixels((x - area.left) / pixel_scale.xstep, builtins.abs(pixel_scale.xstep)),
|
|
339
|
+
round_down_pixels((y - area.top) / pixel_scale.ystep, builtins.abs(pixel_scale.ystep)),
|
|
340
|
+
)
|
|
341
|
+
|
|
293
342
|
|
|
294
343
|
class LayerOperation(LayerMathMixin):
|
|
295
344
|
|
|
@@ -654,8 +703,8 @@ class LayerOperation(LayerMathMixin):
|
|
|
654
703
|
for yoffset in range(0, computation_window.ysize, self.ystep):
|
|
655
704
|
if callback:
|
|
656
705
|
callback(yoffset / computation_window.ysize)
|
|
657
|
-
step=self.ystep
|
|
658
|
-
if yoffset+step > computation_window.ysize:
|
|
706
|
+
step = self.ystep
|
|
707
|
+
if yoffset + step > computation_window.ysize:
|
|
659
708
|
step = computation_window.ysize - yoffset
|
|
660
709
|
chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
|
|
661
710
|
if isinstance(chunk, (float, int)):
|
|
@@ -935,6 +984,48 @@ class LayerOperation(LayerMathMixin):
|
|
|
935
984
|
|
|
936
985
|
return result
|
|
937
986
|
|
|
987
|
+
def read_array(self, x: int, y: int, width: int, height: int) -> np.ndarray:
|
|
988
|
+
"""Read an area of pixles from the specified area of a calculated raster.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
x: X axis offset for reading
|
|
992
|
+
y: Y axis offset for reading
|
|
993
|
+
width: Width of data to read
|
|
994
|
+
height: Height of data to read
|
|
995
|
+
|
|
996
|
+
Returns:
|
|
997
|
+
A numpy array containing the requested data. If the region of data read goes
|
|
998
|
+
beyond the bounds of the calculation that area will be filled with zeros.
|
|
999
|
+
"""
|
|
1000
|
+
projection = self.map_projection
|
|
1001
|
+
if projection is None:
|
|
1002
|
+
raise ValueError("No map projection specified for layers in expression")
|
|
1003
|
+
|
|
1004
|
+
computation_window = Window(0, 0, width, height)
|
|
1005
|
+
expression_area = self.area
|
|
1006
|
+
pixel_scale = projection.scale
|
|
1007
|
+
left = expression_area.left + (x * pixel_scale.xstep)
|
|
1008
|
+
top = expression_area.top + (y * pixel_scale.ystep)
|
|
1009
|
+
computation_area = Area(
|
|
1010
|
+
left=left,
|
|
1011
|
+
top=top,
|
|
1012
|
+
right=left + (width * pixel_scale.xstep),
|
|
1013
|
+
bottom=top + (height * pixel_scale.ystep),
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
chunks = []
|
|
1017
|
+
for yoffset in range(0, height, self.ystep):
|
|
1018
|
+
step = self.ystep
|
|
1019
|
+
if yoffset + step > height:
|
|
1020
|
+
step = height - yoffset
|
|
1021
|
+
chunk = self._eval(computation_area, projection, yoffset, step, computation_window)
|
|
1022
|
+
if isinstance(chunk, (float, int)):
|
|
1023
|
+
chunk = backend.full((step, computation_window.xsize), chunk)
|
|
1024
|
+
chunks.append(chunk)
|
|
1025
|
+
res = np.vstack(chunks)
|
|
1026
|
+
|
|
1027
|
+
return res
|
|
1028
|
+
|
|
938
1029
|
class ShaderStyleOperation(LayerOperation):
|
|
939
1030
|
|
|
940
1031
|
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')
|
|
@@ -331,22 +331,3 @@ class YirgacheffeLayer(LayerMathMixin):
|
|
|
331
331
|
"""
|
|
332
332
|
res = self._read_array(x, y, width, height)
|
|
333
333
|
return backend.demote_array(res)
|
|
334
|
-
|
|
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
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
def pixel_for_latlng(self, lat: float, lng: float) -> tuple[int, int]:
|
|
345
|
-
"""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")
|
|
349
|
-
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)),
|
|
352
|
-
)
|
|
@@ -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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yirgacheffe
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.1
|
|
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.
|
|
@@ -4,7 +4,6 @@ README.md
|
|
|
4
4
|
pyproject.toml
|
|
5
5
|
tests/test_area.py
|
|
6
6
|
tests/test_auto_windowing.py
|
|
7
|
-
tests/test_base.py
|
|
8
7
|
tests/test_constants.py
|
|
9
8
|
tests/test_datatypes.py
|
|
10
9
|
tests/test_group.py
|
|
@@ -17,6 +16,7 @@ tests/test_operators.py
|
|
|
17
16
|
tests/test_optimisation.py
|
|
18
17
|
tests/test_parallel_operators.py
|
|
19
18
|
tests/test_pickle.py
|
|
19
|
+
tests/test_pixel_coord.py
|
|
20
20
|
tests/test_projection.py
|
|
21
21
|
tests/test_raster.py
|
|
22
22
|
tests/test_rescaling.py
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|