|
| 1 | +"""Functions and tools for subsetting a geometry object.""" |
| 2 | +from __future__ import annotations |
| 3 | + |
| 4 | +import math |
| 5 | +from typing import TYPE_CHECKING, Any |
| 6 | + |
| 7 | +import numpy as np |
| 8 | + |
| 9 | +# this caching module imports the geometries so this subset module |
| 10 | +# must be imported inside functions in the geometry modules if needed |
| 11 | +# to avoid circular dependencies |
| 12 | +from pyresample._caching import cache_to_json_if |
| 13 | +from pyresample.boundary import Boundary |
| 14 | +from pyresample.geometry import get_geostationary_bounding_box_in_lonlats, logger |
| 15 | +from pyresample.utils import check_slice_orientation |
| 16 | + |
| 17 | +if TYPE_CHECKING: |
| 18 | + from pyresample import AreaDefinition |
| 19 | + |
| 20 | + |
| 21 | +@cache_to_json_if("cache_geometry_slices") |
| 22 | +def get_area_slices( |
| 23 | + src_area: AreaDefinition, |
| 24 | + area_to_cover: AreaDefinition, |
| 25 | + shape_divisible_by: int | None, |
| 26 | +) -> tuple[slice, slice]: |
| 27 | + """Compute the slice to read based on an `area_to_cover`.""" |
| 28 | + if not _is_area_like(src_area): |
| 29 | + raise NotImplementedError(f"Only AreaDefinitions are supported, not {type(src_area)}") |
| 30 | + if not _is_area_like(area_to_cover): |
| 31 | + raise NotImplementedError(f"Only AreaDefinitions are supported, not {type(area_to_cover)}") |
| 32 | + |
| 33 | + # Intersection only required for two different projections |
| 34 | + proj_def_to_cover = area_to_cover.crs |
| 35 | + proj_def = src_area.crs |
| 36 | + if proj_def_to_cover == proj_def: |
| 37 | + logger.debug('Projections for data and slice areas are identical: %s', |
| 38 | + proj_def_to_cover) |
| 39 | + # Get slice parameters |
| 40 | + xstart, xstop, ystart, ystop = _get_slice_starts_stops(src_area, area_to_cover) |
| 41 | + |
| 42 | + x_slice = check_slice_orientation(slice(xstart, xstop)) |
| 43 | + y_slice = check_slice_orientation(slice(ystart, ystop)) |
| 44 | + x_slice = _ensure_integer_slice(x_slice) |
| 45 | + y_slice = _ensure_integer_slice(y_slice) |
| 46 | + return x_slice, y_slice |
| 47 | + |
| 48 | + data_boundary = _get_area_boundary(src_area) |
| 49 | + area_boundary = _get_area_boundary(area_to_cover) |
| 50 | + intersection = data_boundary.contour_poly.intersection( |
| 51 | + area_boundary.contour_poly) |
| 52 | + if intersection is None: |
| 53 | + logger.debug('Cannot determine appropriate slicing. ' |
| 54 | + "Data and projection area do not overlap.") |
| 55 | + raise NotImplementedError |
| 56 | + x, y = src_area.get_array_indices_from_lonlat( |
| 57 | + np.rad2deg(intersection.lon), np.rad2deg(intersection.lat)) |
| 58 | + x_slice = slice(np.ma.min(x), np.ma.max(x) + 1) |
| 59 | + y_slice = slice(np.ma.min(y), np.ma.max(y) + 1) |
| 60 | + x_slice = _ensure_integer_slice(x_slice) |
| 61 | + y_slice = _ensure_integer_slice(y_slice) |
| 62 | + if shape_divisible_by is not None: |
| 63 | + x_slice = _make_slice_divisible(x_slice, src_area.width, |
| 64 | + factor=shape_divisible_by) |
| 65 | + y_slice = _make_slice_divisible(y_slice, src_area.height, |
| 66 | + factor=shape_divisible_by) |
| 67 | + |
| 68 | + return (check_slice_orientation(x_slice), |
| 69 | + check_slice_orientation(y_slice)) |
| 70 | + |
| 71 | + |
| 72 | +def _is_area_like(area_obj: Any) -> bool: |
| 73 | + return hasattr(area_obj, "crs") and hasattr(area_obj, "area_extent") |
| 74 | + |
| 75 | + |
| 76 | +def _get_slice_starts_stops(src_area, area_to_cover): |
| 77 | + """Get x and y start and stop points for slicing.""" |
| 78 | + llx, lly, urx, ury = area_to_cover.area_extent |
| 79 | + x, y = src_area.get_array_coordinates_from_projection_coordinates([llx, urx], [lly, ury]) |
| 80 | + |
| 81 | + # we use `round` because we want the *exterior* of the pixels to contain the area_to_cover's area extent. |
| 82 | + if (src_area.area_extent[0] > src_area.area_extent[2]) ^ (llx > urx): |
| 83 | + xstart = max(0, round(x[1])) |
| 84 | + xstop = min(src_area.width, round(x[0]) + 1) |
| 85 | + else: |
| 86 | + xstart = max(0, round(x[0])) |
| 87 | + xstop = min(src_area.width, round(x[1]) + 1) |
| 88 | + if (src_area.area_extent[1] > src_area.area_extent[3]) ^ (lly > ury): |
| 89 | + ystart = max(0, round(y[0])) |
| 90 | + ystop = min(src_area.height, round(y[1]) + 1) |
| 91 | + else: |
| 92 | + ystart = max(0, round(y[1])) |
| 93 | + ystop = min(src_area.height, round(y[0]) + 1) |
| 94 | + |
| 95 | + return xstart, xstop, ystart, ystop |
| 96 | + |
| 97 | + |
| 98 | +def _get_area_boundary(area_to_cover: AreaDefinition) -> Boundary: |
| 99 | + try: |
| 100 | + if area_to_cover.is_geostationary: |
| 101 | + return Boundary(*get_geostationary_bounding_box_in_lonlats(area_to_cover)) |
| 102 | + boundary_shape = max(max(*area_to_cover.shape) // 100 + 1, 3) |
| 103 | + return area_to_cover.boundary(frequency=boundary_shape, force_clockwise=True) |
| 104 | + except ValueError: |
| 105 | + raise NotImplementedError("Can't determine boundary of area to cover") |
| 106 | + |
| 107 | + |
| 108 | +def _make_slice_divisible(sli, max_size, factor=2): |
| 109 | + """Make the given slice even in size.""" |
| 110 | + rem = (sli.stop - sli.start) % factor |
| 111 | + if rem != 0: |
| 112 | + adj = factor - rem |
| 113 | + if sli.stop + 1 + rem < max_size: |
| 114 | + sli = slice(sli.start, sli.stop + adj) |
| 115 | + elif sli.start > 0: |
| 116 | + sli = slice(sli.start - adj, sli.stop) |
| 117 | + else: |
| 118 | + sli = slice(sli.start, sli.stop - rem) |
| 119 | + |
| 120 | + return sli |
| 121 | + |
| 122 | + |
| 123 | +def _ensure_integer_slice(sli): |
| 124 | + start = sli.start |
| 125 | + stop = sli.stop |
| 126 | + step = sli.step |
| 127 | + return slice( |
| 128 | + math.floor(start) if start is not None else None, |
| 129 | + math.ceil(stop) if stop is not None else None, |
| 130 | + math.floor(step) if step is not None else None |
| 131 | + ) |
0 commit comments