yirgacheffe 1.8.0__py3-none-any.whl → 1.9.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -15,5 +15,6 @@ except ModuleNotFoundError:
15
15
  from ._core import read_raster, read_rasters, read_shape, read_shape_like, constant, read_narrow_raster
16
16
  from .constants import WGS_84_PROJECTION
17
17
  from .window import Area, MapProjection, Window
18
+ from ._backends.enumeration import dtype as DataType
18
19
 
19
20
  gdal.UseExceptions()
@@ -41,6 +41,26 @@ class operators(Enum):
41
41
  ISNAN = 36
42
42
 
43
43
  class dtype(Enum):
44
+ """Represents the type of data returned by a layer.
45
+
46
+ This enumeration defines the valid data types supported by Yirgacheffe, and is
47
+ what is returned by calling `datatype` on a layer or expression, and can be
48
+ passed to `astype` to convert values between types.
49
+
50
+ Attributes:
51
+ Float32: 32 bit floating point value
52
+ Float64: 64 bit floating point value
53
+ Byte: Unsigned 8 bit integer value
54
+ Int8: Signed 8 bit integer value
55
+ Int16: Signed 16 bit integer value
56
+ Int32: Signed 32 bit integer value
57
+ Int64: Signed 64 bit integer value
58
+ UInt8: Unsigned 8 bit integer value
59
+ UInt16: Unsigned 16 bit integer value
60
+ UInt32: Unsigned 32 bit integer value
61
+ UInt64: Unsigned 64 bit integer value
62
+ """
63
+
44
64
  Float32 = gdal.GDT_Float32
45
65
  Float64 = gdal.GDT_Float64
46
66
  Byte = gdal.GDT_Byte
yirgacheffe/_operators.py CHANGED
@@ -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):
yirgacheffe/constants.py CHANGED
@@ -1,8 +1,8 @@
1
+ import pyproj
2
+
1
3
  YSTEP = 512
2
4
  MINIMUM_CHUNKS_PER_THREAD = 1
3
5
 
4
6
  # 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"]]'
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
  )
@@ -333,36 +333,36 @@ class VectorLayer(YirgacheffeLayer):
333
333
  self._anchor = anchor
334
334
  self._envelopes = envelopes
335
335
 
336
- # if projection is not None:
337
- # # Get the area, but scale it to the pixel resolution that we're using. Note that
338
- # # the pixel scale GDAL uses can have -ve values, but those will mess up the
339
- # # ceil/floor math, so we use absolute versions when trying to round.
340
- # abs_xstep, abs_ystep = abs(projection.xstep), abs(projection.ystep)
341
- #
342
- # # Lacking any other reference, we will make the raster align with
343
- # # (0.0, 0.0), if sometimes we want to align with an existing raster, so if
344
- # # an anchor is specified, ensure we use that as our pixel space alignment
345
- # x_anchor = anchor[0]
346
- # y_anchor = anchor[1]
347
- # left_shift = x_anchor - abs_xstep
348
- # right_shift = x_anchor
349
- # top_shift = y_anchor
350
- # bottom_shift = y_anchor - abs_ystep
351
- #
352
- # area = Area(
353
- # left=(floor((min(x[0] for x in envelopes) - left_shift) / abs_xstep) * abs_xstep) + left_shift,
354
- # top=(ceil((max(x[3] for x in envelopes) - top_shift) / abs_ystep) * abs_ystep) + top_shift,
355
- # right=(ceil((max(x[1] for x in envelopes) - right_shift) / abs_xstep) * abs_xstep) + right_shift,
356
- # bottom=(floor((min(x[2] for x in envelopes) - bottom_shift) / abs_ystep) * abs_ystep) + bottom_shift,
357
- # )
358
- # else:
336
+ if projection is not None:
337
+ # Get the area, but scale it to the pixel resolution that we're using. Note that
338
+ # the pixel scale GDAL uses can have -ve values, but those will mess up the
339
+ # ceil/floor math, so we use absolute versions when trying to round.
340
+ abs_xstep, abs_ystep = abs(projection.xstep), abs(projection.ystep)
341
+
342
+ # Lacking any other reference, we will make the raster align with
343
+ # (0.0, 0.0), if sometimes we want to align with an existing raster, so if
344
+ # an anchor is specified, ensure we use that as our pixel space alignment
345
+ x_anchor = anchor[0]
346
+ y_anchor = anchor[1]
347
+ left_shift = x_anchor - abs_xstep
348
+ right_shift = x_anchor
349
+ top_shift = y_anchor
350
+ bottom_shift = y_anchor - abs_ystep
351
+
352
+ area = Area(
353
+ left=(floor((min(x[0] for x in envelopes) - left_shift) / abs_xstep) * abs_xstep) + left_shift,
354
+ top=(ceil((max(x[3] for x in envelopes) - top_shift) / abs_ystep) * abs_ystep) + top_shift,
355
+ right=(ceil((max(x[1] for x in envelopes) - right_shift) / abs_xstep) * abs_xstep) + right_shift,
356
+ bottom=(floor((min(x[2] for x in envelopes) - bottom_shift) / abs_ystep) * abs_ystep) + bottom_shift,
357
+ )
358
+ else:
359
359
  # If we don't have a projection just go with the idealised area
360
- area = Area(
361
- left=floor(min(x[0] for x in envelopes)),
362
- top=ceil(max(x[3] for x in envelopes)),
363
- right=ceil(max(x[1] for x in envelopes)),
364
- bottom=floor(min(x[2] for x in envelopes)),
365
- )
360
+ area = Area(
361
+ left=floor(min(x[0] for x in envelopes)),
362
+ top=ceil(max(x[3] for x in envelopes)),
363
+ right=ceil(max(x[1] for x in envelopes)),
364
+ bottom=floor(min(x[2] for x in envelopes)),
365
+ )
366
366
 
367
367
  super().__init__(area, projection)
368
368
 
yirgacheffe/window.py CHANGED
@@ -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)
@@ -72,7 +85,9 @@ class Area:
72
85
  def __hash__(self):
73
86
  return (self.left, self.top, self.right, self.bottom).__hash__()
74
87
 
75
- def __eq__(self, other) -> bool:
88
+ def __eq__(self, other: object) -> bool:
89
+ if not isinstance(other, Area):
90
+ return False
76
91
  return math.isclose(self.left, other.left, abs_tol=1e-09) and \
77
92
  math.isclose(self.right, other.right, abs_tol=1e-09) and \
78
93
  math.isclose(self.top, other.top, abs_tol=1e-09) and \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.8.0
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,27 +1,27 @@
1
- yirgacheffe/__init__.py,sha256=SBRDdk_GEIN6tOxxGrNySjT9ympv9FL6thEgsiycODo,615
1
+ yirgacheffe/__init__.py,sha256=OOzfXtafPoDpAsNRC08BXjmwv0hBp-mNFCjwplGs9lY,668
2
2
  yirgacheffe/_core.py,sha256=AU6tlqovBV_l1dNZs6AlHSw59Z0U6pStUaQZvJGiLhM,5721
3
- yirgacheffe/_operators.py,sha256=OiR4pCVILmdXDmG37YILJuYjcxZlRussrJC7DeyoOts,36070
4
- yirgacheffe/constants.py,sha256=uCWJwec3-ND-zVxYbsk1sdHKANl3ToNCTPg7MZb0j2g,434
3
+ yirgacheffe/_operators.py,sha256=cnJgnfnfIRwrPB6sX2TVU_-s4eDyhZCPyemwIBFurxU,37723
4
+ yirgacheffe/constants.py,sha256=bKUjOGNj19zwggV79lJgK7tiv51DH2-rgNOKswl2gvQ,293
5
5
  yirgacheffe/operators.py,sha256=nw-BpnAwTjCwFtjosa8wKd2MGUuC0PJR5jACFdLhqCg,412
6
6
  yirgacheffe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  yirgacheffe/rounding.py,sha256=ZNuAaxsWfzYETC_G9H5weY1ZOci2pihEKTVrUiIqfZw,2257
8
- yirgacheffe/window.py,sha256=-5eB3rnVUr7p_A2VKt_DqR-ACDeNwRt6X0_UEiVLCo8,8913
8
+ yirgacheffe/window.py,sha256=QuyBLOwKFI0XkEQ4Bd2hdELPbJSfHL7mt5KSi7CIHcE,9505
9
9
  yirgacheffe/_backends/__init__.py,sha256=jN-2iRrHStnPI6cNL7XhwhsROtI0EaGfIrbF5c-ECV0,334
10
- yirgacheffe/_backends/enumeration.py,sha256=Jrce2p2n4Wlk5tHBkiWntDnpLSD_0H-bnwgsKXHjkwQ,1018
10
+ yirgacheffe/_backends/enumeration.py,sha256=9bcCXz9Ssrh8Oh1iazodkx6Gm2kQBi9HQ9z9zehS4AE,1806
11
11
  yirgacheffe/_backends/mlx.py,sha256=U1gl1lK1mZXLEET6ylF1TNs6WJ0PBEvfSk7ppn28n8w,6203
12
12
  yirgacheffe/_backends/numpy.py,sha256=Gxx49JJH79GFEkKIpV6IyjCUcdtN5-qLlzRfylzKhS4,4142
13
13
  yirgacheffe/layers/__init__.py,sha256=mYKjw5YTcMNv_hMy7a6K4yRzIuNUbR8WuBTw4WIAmSk,435
14
14
  yirgacheffe/layers/area.py,sha256=wJcMHbLJBaXS4BeFbu5rYeKfgu3gvaE9hwQ5j6aw-y4,3976
15
- yirgacheffe/layers/base.py,sha256=dQQLFKP05QK4C73N1DCDYT8D8eD15HuSQKN5KrABeHg,14289
15
+ yirgacheffe/layers/base.py,sha256=EkfR3DOQ1I_tQZfpOSczs-sWFSP6uUMXdiB7xspfd4E,14954
16
16
  yirgacheffe/layers/constant.py,sha256=gtkQ98Z01CYYDgFElswtRZY4ZG3UnS5NIAoIVue5ufk,1481
17
17
  yirgacheffe/layers/group.py,sha256=yaqf-ra_Vh59yrWcz7-OvJ1fBnTcBXZd18AfRDN5Ymo,16157
18
18
  yirgacheffe/layers/h3layer.py,sha256=Rq1bFo7CApIh5NdBcV7hSj3hm-DszY79nhYsTRAvJ_g,9916
19
19
  yirgacheffe/layers/rasters.py,sha256=zBE9uXm6LvAQF2_XdQzcOgJQOQWGmuPflY5JNDrUf3k,13527
20
20
  yirgacheffe/layers/rescaled.py,sha256=gEFbXeYxX1nVn7eQYmbGww90_yc5ENmgQrD_WxXxpQE,3352
21
- yirgacheffe/layers/vectors.py,sha256=AD1KcXO5acFZ9CTRENTPA_D9DMGSjAxGIF298LdeNeY,20010
22
- yirgacheffe-1.8.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
23
- yirgacheffe-1.8.0.dist-info/METADATA,sha256=wcJXfOQ15YfLkT-0TvivPXKZnyanK2wEQsalRY6f2Yk,23797
24
- yirgacheffe-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- yirgacheffe-1.8.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
26
- yirgacheffe-1.8.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
27
- yirgacheffe-1.8.0.dist-info/RECORD,,
21
+ yirgacheffe/layers/vectors.py,sha256=A27kuTr0C9BZhHG0-cplNEa7aSNcse37Pm9xTjEzv-c,19990
22
+ yirgacheffe-1.9.0.dist-info/licenses/LICENSE,sha256=dNSHwUCJr6axStTKDEdnJtfmDdFqlE3h1NPCveqPfnY,757
23
+ yirgacheffe-1.9.0.dist-info/METADATA,sha256=ciRPPO-k-vbyd0gbg_u-uinc739ZoPuazQna9MpluCM,24186
24
+ yirgacheffe-1.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ yirgacheffe-1.9.0.dist-info/entry_points.txt,sha256=j4KgHXbVGbGyfTySc1ypBdERpfihO4WNjppvCdE9HjE,52
26
+ yirgacheffe-1.9.0.dist-info/top_level.txt,sha256=9DBFlKO2Ld3hG6TuE3qOTd3Tt8ugTiXil4AN4Wr9_y0,12
27
+ yirgacheffe-1.9.0.dist-info/RECORD,,