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.

Files changed (60) hide show
  1. {yirgacheffe-1.8.1/yirgacheffe.egg-info → yirgacheffe-1.9.1}/PKG-INFO +7 -1
  2. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/README.md +5 -0
  3. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/pyproject.toml +2 -1
  4. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_h3layer.py +1 -1
  5. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_openers.py +1 -3
  6. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_operators.py +81 -0
  7. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_pickle.py +1 -1
  8. yirgacheffe-1.8.1/tests/test_base.py → yirgacheffe-1.9.1/tests/test_pixel_coord.py +90 -25
  9. yirgacheffe-1.9.1/tests/test_projection.py +45 -0
  10. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_vectors.py +2 -4
  11. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_operators.py +93 -2
  12. yirgacheffe-1.9.1/yirgacheffe/constants.py +8 -0
  13. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/base.py +0 -19
  14. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/window.py +20 -7
  15. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1/yirgacheffe.egg-info}/PKG-INFO +7 -1
  16. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/SOURCES.txt +1 -1
  17. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/requires.txt +1 -0
  18. yirgacheffe-1.8.1/tests/test_projection.py +0 -32
  19. yirgacheffe-1.8.1/yirgacheffe/constants.py +0 -8
  20. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/LICENSE +0 -0
  21. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/MANIFEST.in +0 -0
  22. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/setup.cfg +0 -0
  23. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_area.py +0 -0
  24. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_auto_windowing.py +0 -0
  25. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_constants.py +0 -0
  26. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_datatypes.py +0 -0
  27. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_group.py +0 -0
  28. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_intersection.py +0 -0
  29. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_multiband.py +0 -0
  30. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_nodata.py +0 -0
  31. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_optimisation.py +0 -0
  32. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_parallel_operators.py +0 -0
  33. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_raster.py +0 -0
  34. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_rescaling.py +0 -0
  35. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_rounding.py +0 -0
  36. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_save_with_window.py +0 -0
  37. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_sum_with_window.py +0 -0
  38. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_uniform_area_layer.py +0 -0
  39. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_union.py +0 -0
  40. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/tests/test_window.py +0 -0
  41. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/__init__.py +0 -0
  42. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/__init__.py +0 -0
  43. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/enumeration.py +0 -0
  44. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/mlx.py +0 -0
  45. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_backends/numpy.py +0 -0
  46. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/_core.py +0 -0
  47. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/__init__.py +0 -0
  48. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/area.py +0 -0
  49. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/constant.py +0 -0
  50. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/group.py +0 -0
  51. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/h3layer.py +0 -0
  52. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/rasters.py +0 -0
  53. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/rescaled.py +0 -0
  54. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/layers/vectors.py +0 -0
  55. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/operators.py +0 -0
  56. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/py.typed +0 -0
  57. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe/rounding.py +0 -0
  58. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  59. {yirgacheffe-1.8.1 → yirgacheffe-1.9.1}/yirgacheffe.egg-info/entry_points.txt +0 -0
  60. {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.8.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
+ [![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.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.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
 
@@ -1,136 +1,196 @@
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
 
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(area: Area, coord: tuple[float, float], expected: tuple[int, int]) -> None:
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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
- MapProjection(WGS_84_PROJECTION, 0.2, -0.2),
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.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:
@@ -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
- 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.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
+ [![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.
@@ -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
@@ -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