• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine-python / 19573839061

21 Nov 2025 02:37PM UTC coverage: 78.668% (-1.1%) from 79.74%
19573839061

Pull #221

github

web-flow
Merge 4012cbccd into e06d48b64
Pull Request #221: Pixel_based_queries_rewrite

3083 of 3919 relevant lines covered (78.67%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.63
geoengine/types.py
1
# pylint: disable=too-many-lines
2

3
"""
4
Different type mappings of geo engine types
5
"""
6

7
from __future__ import annotations
1✔
8

9
from abc import abstractmethod
1✔
10
from datetime import datetime, timezone
1✔
11
from enum import Enum
1✔
12
from typing import Any, Literal, cast
1✔
13
from uuid import UUID
1✔
14

15
import geoengine_openapi_client
1✔
16
import geoengine_openapi_client.models
1✔
17
import geoengine_openapi_client.models.raster_to_dataset_query_rectangle
1✔
18
import numpy as np
1✔
19
from attr import dataclass
1✔
20

21
from geoengine.colorizer import Colorizer
1✔
22
from geoengine.error import GeoEngineException, InputException, TypeException
1✔
23

24
DEFAULT_ISO_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
1✔
25

26

27
class SpatialBounds:
1✔
28
    """A spatial bounds object"""
29

30
    xmin: float
1✔
31
    ymin: float
1✔
32
    xmax: float
1✔
33
    ymax: float
1✔
34

35
    def __init__(self, xmin: float, ymin: float, xmax: float, ymax: float) -> None:
1✔
36
        """Initialize a new `SpatialBounds` object"""
37
        if (xmin > xmax) or (ymin > ymax):
1✔
38
            raise InputException("Bbox: Malformed since min must be <= max")
×
39

40
        self.xmin = xmin
1✔
41
        self.ymin = ymin
1✔
42
        self.xmax = xmax
1✔
43
        self.ymax = ymax
1✔
44

45
    def as_bbox_str(self, y_axis_first=False) -> str:
1✔
46
        """
47
        A comma-separated string representation of the spatial bounds with OGC axis ordering
48
        """
49
        bbox_tuple = self.as_bbox_tuple(y_axis_first=y_axis_first)
1✔
50
        return f"{bbox_tuple[0]},{bbox_tuple[1]},{bbox_tuple[2]},{bbox_tuple[3]}"
1✔
51

52
    def as_bbox_tuple(self, y_axis_first=False) -> tuple[float, float, float, float]:
1✔
53
        """
54
        Return the bbox with OGC axis ordering of the srs
55
        """
56

57
        if y_axis_first:
1✔
58
            return (self.ymin, self.xmin, self.ymax, self.xmax)
1✔
59

60
        return (self.xmin, self.ymin, self.xmax, self.ymax)
1✔
61

62
    def x_axis_size(self) -> float:
1✔
63
        """The size of the x axis"""
64
        return self.xmax - self.xmin
×
65

66
    def y_axis_size(self) -> float:
1✔
67
        """The size of the y axis"""
68
        return self.ymax - self.ymin
×
69

70

71
class BoundingBox2D(SpatialBounds):
1✔
72
    """'A 2D bounding box."""
73

74
    def to_api_dict(self) -> geoengine_openapi_client.BoundingBox2D:
1✔
75
        return geoengine_openapi_client.BoundingBox2D(
1✔
76
            lower_left_coordinate=geoengine_openapi_client.Coordinate2D(
77
                x=self.xmin,
78
                y=self.ymin,
79
            ),
80
            upper_right_coordinate=geoengine_openapi_client.Coordinate2D(
81
                x=self.xmax,
82
                y=self.ymax,
83
            ),
84
        )
85

86
    def intersection(self, other: BoundingBox2D) -> BoundingBox2D:
1✔
87
        return BoundingBox2D(
×
88
            min(self.xmin, other.xmin),
89
            min(self.ymin, other.ymin),
90
            max(self.xmax, other.xmax),
91
            max(self.ymax, other.ymax),
92
        )
93

94
    @staticmethod
1✔
95
    def from_response(response: geoengine_openapi_client.BoundingBox2D) -> BoundingBox2D:
1✔
96
        """create a `BoundingBox2D` from an API response"""
97
        lower_left = response.lower_left_coordinate
1✔
98
        upper_right = response.upper_right_coordinate
1✔
99

100
        return BoundingBox2D(
1✔
101
            lower_left.x,
102
            lower_left.y,
103
            upper_right.x,
104
            upper_right.y,
105
        )
106

107
    def __repr__(self) -> str:
1✔
108
        return f"BoundingBox2D(xmin={self.xmin}, ymin={self.ymin}, xmax={self.xmax}, ymax={self.ymax})"
×
109

110

111
class SpatialPartition2D(SpatialBounds):
1✔
112
    """A 2D spatial partition."""
113

114
    @staticmethod
1✔
115
    def from_response(response: geoengine_openapi_client.SpatialPartition2D) -> SpatialPartition2D:
1✔
116
        """create a `SpatialPartition2D` from an API response"""
117
        upper_left = response.upper_left_coordinate
×
118
        lower_right = response.lower_right_coordinate
×
119

120
        return SpatialPartition2D(
×
121
            upper_left.x,
122
            lower_right.y,
123
            lower_right.x,
124
            upper_left.y,
125
        )
126

127
    def to_api_dict(self) -> geoengine_openapi_client.SpatialPartition2D:
1✔
128
        return geoengine_openapi_client.SpatialPartition2D(
1✔
129
            upper_left_coordinate=geoengine_openapi_client.Coordinate2D(
130
                x=self.xmin,
131
                y=self.ymax,
132
            ),
133
            lower_right_coordinate=geoengine_openapi_client.Coordinate2D(
134
                x=self.xmax,
135
                y=self.ymin,
136
            ),
137
        )
138

139
    def to_bounding_box(self) -> BoundingBox2D:
1✔
140
        """convert to a `BoundingBox2D`"""
141
        return BoundingBox2D(self.xmin, self.ymin, self.xmax, self.ymax)
1✔
142

143
    @staticmethod
1✔
144
    def from_bounding_box(bbox: BoundingBox2D) -> SpatialPartition2D:
1✔
145
        """Creates a  `SpatialPartition2D` from a `BoundingBox2D`"""
146
        return SpatialPartition2D(bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax)
1✔
147

148
    def __repr__(self) -> str:
1✔
149
        return f"SpatialPartition2D(xmin={self.xmin}, ymin={self.ymin}, xmax={self.xmax}, ymax={self.ymax})"
×
150

151

152
class TimeInterval:
1✔
153
    """'A time interval."""
154

155
    start: np.datetime64
1✔
156
    end: np.datetime64 | None
1✔
157

158
    def __init__(self, start: datetime | np.datetime64, end: datetime | np.datetime64 | None = None) -> None:
1✔
159
        """Initialize a new `TimeInterval` object"""
160

161
        if isinstance(start, np.datetime64):
1✔
162
            self.start = start
1✔
163
        elif isinstance(start, datetime):
1✔
164
            # We assume that a datetime without a timezone means UTC
165
            if start.tzinfo is not None:
1✔
166
                start = start.astimezone(tz=timezone.utc).replace(tzinfo=None)
1✔
167
            self.start = np.datetime64(start)
1✔
168
        else:
169
            raise InputException("`start` must be of type `datetime.datetime` or `numpy.datetime64`")
×
170

171
        if end is None:
1✔
172
            self.end = self.start
1✔
173
        elif isinstance(end, np.datetime64):
1✔
174
            self.end = end
1✔
175
        elif isinstance(end, datetime):
1✔
176
            # We assume that a datetime without a timezone means UTC
177
            if end.tzinfo is not None:
1✔
178
                end = end.astimezone(tz=timezone.utc).replace(tzinfo=None)
1✔
179
            self.end = np.datetime64(end)
1✔
180
        else:
181
            raise InputException("`end` must be of type `datetime.datetime` or `numpy.datetime64`")
×
182

183
        # Check validity of time interval if an `end` exists
184
        if end is not None and start > end:
1✔
185
            raise InputException("Time inverval: Start must be <= End")
1✔
186

187
    def is_instant(self) -> bool:
1✔
188
        return self.end is None or self.start == self.end
×
189

190
    @property
1✔
191
    def time_str(self) -> str:
1✔
192
        """
193
        Return the time instance or interval as a string representation
194
        """
195

196
        start_iso = TimeInterval.__datetime_to_iso_str(self.start)
1✔
197

198
        if self.end is None or self.start == self.end:
1✔
199
            return start_iso
1✔
200

201
        end_iso = TimeInterval.__datetime_to_iso_str(self.end)
1✔
202

203
        return start_iso + "/" + end_iso
1✔
204

205
    @staticmethod
1✔
206
    def from_response(response: geoengine_openapi_client.models.TimeInterval) -> TimeInterval:
1✔
207
        """create a `TimeInterval` from an API response"""
208

209
        if response.start is None:
1✔
210
            raise TypeException("TimeInterval must have a start")
×
211

212
        start = cast(int, response.start)
1✔
213
        end = None
1✔
214
        if response.end is not None:
1✔
215
            end = cast(int, response.end)
1✔
216

217
        if start == end:
1✔
218
            end = None
1✔
219

220
        return TimeInterval(
1✔
221
            np.datetime64(start, "ms"),
222
            np.datetime64(end, "ms") if end is not None else None,
223
        )
224

225
    def __repr__(self) -> str:
1✔
226
        return f"TimeInterval(start={self.start}, end={self.end})"
1✔
227

228
    def to_api_dict(self) -> geoengine_openapi_client.TimeInterval:
1✔
229
        """create a openapi `TimeInterval` from self"""
230
        start = self.start.astype("datetime64[ms]").astype(int)
1✔
231
        end = self.end.astype("datetime64[ms]").astype(int) if self.end is not None else None
1✔
232

233
        # The openapi Timeinterval does not accept end: None. So we set it to start IF self is an instant.
234
        end = end if end is not None else start
1✔
235

236
        print(self, start, end)
1✔
237

238
        return geoengine_openapi_client.TimeInterval(start=int(start), end=int(end))
1✔
239

240
    @staticmethod
1✔
241
    def __datetime_to_iso_str(timestamp: np.datetime64) -> str:
1✔
242
        return str(np.datetime_as_string(timestamp, unit="ms", timezone="UTC")).replace("Z", "+00:00")
1✔
243

244
    def __eq__(self, other: Any) -> bool:
1✔
245
        """Check if two `TimeInterval` objects are equal."""
246
        if not isinstance(other, TimeInterval):
×
247
            return False
×
248
        return self.start == other.start and self.end == other.end
×
249

250

251
class SpatialResolution:
1✔
252
    """'A spatial resolution."""
253

254
    x_resolution: float
1✔
255
    y_resolution: float
1✔
256

257
    def __init__(self, x_resolution: float, y_resolution: float) -> None:
1✔
258
        """Initialize a new `SpatialResolution` object"""
259
        if x_resolution <= 0 or y_resolution <= 0:
1✔
260
            raise InputException("Resolution: Must be positive")
×
261

262
        self.x_resolution = x_resolution
1✔
263
        self.y_resolution = y_resolution
1✔
264

265
    def to_api_dict(self) -> geoengine_openapi_client.SpatialResolution:
1✔
266
        return geoengine_openapi_client.SpatialResolution(
×
267
            x=self.x_resolution,
268
            y=self.y_resolution,
269
        )
270

271
    @staticmethod
1✔
272
    def from_response(response: geoengine_openapi_client.SpatialResolution) -> SpatialResolution:
1✔
273
        """create a `SpatialResolution` from an API response"""
274
        return SpatialResolution(x_resolution=response.x, y_resolution=response.y)
×
275

276
    def as_tuple(self) -> tuple[float, float]:
1✔
277
        return (self.x_resolution, self.y_resolution)
×
278

279
    def resolution_ogc(self, srs_code: str) -> tuple[float, float]:
1✔
280
        """
281
        Return the resolution in OGC style
282
        """
283

284
        # TODO: why is the y resolution in this case negative but not in all other cases?
285
        if srs_code == "EPSG:4326":
1✔
286
            return (-self.y_resolution, self.x_resolution)
1✔
287

288
        return self.as_tuple()
×
289

290
    def __str__(self) -> str:
1✔
291
        return str(f"{self.x_resolution},{self.y_resolution}")
1✔
292

293
    def __repr__(self) -> str:
1✔
294
        return str(f"SpatialResolution(x={self.x_resolution}, y={self.y_resolution})")
×
295

296

297
class QueryRectangle:
1✔
298
    """
299
    A multi-dimensional query rectangle, consisting of spatial and temporal information.
300
    """
301

302
    __spatial_bounds: BoundingBox2D
1✔
303
    __time_interval: TimeInterval
1✔
304
    __srs: str
1✔
305

306
    def __init__(
1✔
307
        self,
308
        spatial_bounds: BoundingBox2D | SpatialPartition2D | tuple[float, float, float, float],
309
        time_interval: TimeInterval | tuple[datetime, datetime | None],
310
        srs="EPSG:4326",
311
    ) -> None:
312
        """
313
        Initialize a new `QueryRectangle` object
314

315
        Parameters
316
        ----------
317
        spatial_bounds
318
            The spatial bounds of the query rectangle.
319
            Either a `BoundingBox2D` or a tuple of floats (xmin, ymin, xmax, ymax)
320
        time_interval
321
            The time interval of the query rectangle.
322
            Either a `TimeInterval` or a tuple of `datetime.datetime` objects (start, end)
323
        """
324

325
        if not isinstance(spatial_bounds, BoundingBox2D):
1✔
326
            if isinstance(spatial_bounds, SpatialPartition2D):
1✔
327
                spatial_bounds = spatial_bounds.to_bounding_box()
1✔
328
            else:
329
                spatial_bounds = BoundingBox2D(*spatial_bounds)
×
330
        if not isinstance(time_interval, TimeInterval):
1✔
331
            time_interval = TimeInterval(*time_interval)
×
332

333
        self.__spatial_bounds = spatial_bounds
1✔
334
        self.__time_interval = time_interval
1✔
335
        self.__srs = srs
1✔
336

337
    @property
1✔
338
    def bbox_str(self) -> str:
1✔
339
        """
340
        A comma-separated string representation of the spatial bounds
341
        """
342
        return self.__spatial_bounds.as_bbox_str()
1✔
343

344
    @property
1✔
345
    def bbox_ogc_str(self) -> str:
1✔
346
        """
347
        A comma-separated string representation of the spatial bounds with OGC axis ordering
348
        """
349
        y_axis_first = self.__srs == "EPSG:4326"
1✔
350
        return self.__spatial_bounds.as_bbox_str(y_axis_first=y_axis_first)
1✔
351

352
    @property
1✔
353
    def bbox_ogc(self) -> tuple[float, float, float, float]:
1✔
354
        """
355
        Return the bbox with OGC axis ordering of the srs
356
        """
357

358
        # TODO: properly handle axis order
359
        y_axis_first = self.__srs == "EPSG:4326"
1✔
360
        return self.__spatial_bounds.as_bbox_tuple(y_axis_first=y_axis_first)
1✔
361

362
    @property
1✔
363
    def time(self) -> TimeInterval:
1✔
364
        """
365
        Return the time instance or interval
366
        """
367
        return self.__time_interval
1✔
368

369
    @property
1✔
370
    def spatial_bounds(self) -> BoundingBox2D:
1✔
371
        """
372
        Return the spatial bounds
373
        """
374
        return self.__spatial_bounds
1✔
375

376
    @property
1✔
377
    def time_str(self) -> str:
1✔
378
        """
379
        Return the time instance or interval as a string representation
380
        """
381
        return self.time.time_str
1✔
382

383
    @property
1✔
384
    def srs(self) -> str:
1✔
385
        """
386
        Return the SRS string
387
        """
388
        return self.__srs
1✔
389

390
    def __repr__(self) -> str:
1✔
391
        """Return a string representation of the query rectangle."""
392
        r = "QueryRectangle( \n"
×
393
        r += "    " + repr(self.__spatial_bounds) + "\n"
×
394
        r += "    " + repr(self.__time_interval) + "\n"
×
395
        r += f"    srs={self.__srs} \n"
×
396
        r += ")"
×
397
        return r
×
398

399
    def with_raster_bands(self, raster_bands: list[int]) -> RasterQueryRectangle:
1✔
400
        """Converts a `QueryRectangle` into a `RasterQueryRectangle`"""
401
        return RasterQueryRectangle(self.spatial_bounds, self.time, raster_bands, self.srs)
1✔
402

403

404
class RasterQueryRectangle(QueryRectangle):
1✔
405
    """
406
    A multi-dimensional query rectangle, consisting of spatial and temporal information and raster bands.
407
    """
408

409
    __bands: list[int] = []
1✔
410

411
    def __init__(
1✔
412
        self,
413
        spatial_bounds: BoundingBox2D | SpatialPartition2D | tuple[float, float, float, float],
414
        time_interval: TimeInterval | tuple[datetime, datetime | None],
415
        raster_bands: list[int] | None | int,
416
        srs="EPSG:4326",
417
    ) -> None:
418
        """
419
        Initialize a new `QueryRectangle` object
420

421
        Parameters
422
        ----------
423
        spatial_bounds
424
            The spatial bounds of the query rectangle.
425
            Either a `BoundingBox2D` or a tuple of floats (xmin, ymin, xmax, ymax)
426
        time_interval
427
            The time interval of the query rectangle.
428
            Either a `TimeInterval` or a tuple of `datetime.datetime` objects (start, end)
429
        bands
430
            The raster bands of the query rectangle.
431
            A List of ints representing the band numbers.
432
        """
433

434
        super().__init__(spatial_bounds, time_interval, srs)
1✔
435
        if raster_bands is None:
1✔
436
            self.__bands = [0]
×
437
        elif isinstance(raster_bands, int):
1✔
438
            self.__bands = [raster_bands]
×
439
        else:
440
            self.__bands = raster_bands
1✔
441

442
    @property
1✔
443
    def raster_bands(self) -> list[int]:
1✔
444
        """
445
        Return the query bands
446
        """
447
        return self.__bands
1✔
448

449
    def __repr__(self) -> str:
1✔
450
        """Return a string representation of the query rectangle."""
451
        r = "RasterQueryRectangle( \n"
×
452
        r += "    " + repr(self.spatial_bounds) + "\n"
×
453
        r += "    " + repr(self.time) + "\n"
×
454
        r += "    " + repr(self.__bands) + "\n"
×
455
        r += f"    srs={self.srs} \n"
×
456
        r += ")"
×
457
        return r
×
458

459

460
class ResultDescriptor:  # pylint: disable=too-few-public-methods
1✔
461
    """
462
    Base class for result descriptors
463
    """
464

465
    __spatial_reference: str
1✔
466
    __time_bounds: TimeInterval | None
1✔
467

468
    def __init__(
1✔
469
        self,
470
        spatial_reference: str,
471
        time_bounds: TimeInterval | None = None,
472
    ) -> None:
473
        """Initialize a new `ResultDescriptor` object"""
474

475
        self.__spatial_reference = spatial_reference
1✔
476
        self.__time_bounds = time_bounds
1✔
477

478
    @staticmethod
1✔
479
    def from_response(response: geoengine_openapi_client.TypedResultDescriptor) -> ResultDescriptor:
1✔
480
        """
481
        Parse a result descriptor from an http response
482
        """
483

484
        inner = response.actual_instance
1✔
485

486
        if isinstance(inner, geoengine_openapi_client.TypedRasterResultDescriptor):
1✔
487
            return RasterResultDescriptor.from_response_raster(inner)
1✔
488
        if isinstance(inner, geoengine_openapi_client.TypedVectorResultDescriptor):
1✔
489
            return VectorResultDescriptor.from_response_vector(inner)
1✔
490
        if isinstance(inner, geoengine_openapi_client.TypedPlotResultDescriptor):
1✔
491
            return PlotResultDescriptor.from_response_plot(inner)
1✔
492

493
        raise TypeException("Unknown `ResultDescriptor` type")
×
494

495
    @classmethod
1✔
496
    def is_raster_result(cls) -> bool:
1✔
497
        """
498
        Return true if the result is of type raster
499
        """
500
        return False
×
501

502
    @classmethod
1✔
503
    def is_vector_result(cls) -> bool:
1✔
504
        """
505
        Return true if the result is of type vector
506
        """
507
        return False
1✔
508

509
    @classmethod
1✔
510
    def is_plot_result(cls) -> bool:
1✔
511
        """
512
        Return true if the result is of type plot
513
        """
514

515
        return False
×
516

517
    @property
1✔
518
    def spatial_reference(self) -> str:
1✔
519
        """Return the spatial reference"""
520

521
        return self.__spatial_reference
1✔
522

523
    @property
1✔
524
    def time_bounds(self) -> TimeInterval | None:
1✔
525
        """Return the time bounds"""
526

527
        return self.__time_bounds
1✔
528

529
    @abstractmethod
1✔
530
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
531
        pass
×
532

533
    def __iter__(self):
1✔
534
        return iter(self.to_api_dict().items())
×
535

536

537
class VectorResultDescriptor(ResultDescriptor):
1✔
538
    """
539
    A vector result descriptor
540
    """
541

542
    __spatial_bounds: BoundingBox2D | None
1✔
543
    __data_type: VectorDataType
1✔
544
    __columns: dict[str, VectorColumnInfo]
1✔
545

546
    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
1✔
547
        self,
548
        spatial_reference: str,
549
        data_type: VectorDataType,
550
        columns: dict[str, VectorColumnInfo],
551
        time_bounds: TimeInterval | None = None,
552
        spatial_bounds: BoundingBox2D | None = None,
553
    ) -> None:
554
        """Initialize a vector result descriptor"""
555
        super().__init__(spatial_reference, time_bounds)
1✔
556
        self.__data_type = data_type
1✔
557
        self.__columns = columns
1✔
558
        self.__spatial_bounds = spatial_bounds
1✔
559

560
    @staticmethod
1✔
561
    def from_response_vector(response: geoengine_openapi_client.TypedVectorResultDescriptor) -> VectorResultDescriptor:
1✔
562
        """Parse a vector result descriptor from an http response"""
563
        sref = response.spatial_reference
1✔
564
        data_type = VectorDataType.from_string(response.data_type)
1✔
565
        columns = {name: VectorColumnInfo.from_response(info) for name, info in response.columns.items()}
1✔
566

567
        time_bounds = None
1✔
568
        if response.time is not None:
1✔
569
            time_bounds = TimeInterval.from_response(response.time)
1✔
570
        spatial_bounds = None
1✔
571
        if response.bbox is not None:
1✔
572
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
1✔
573

574
        return VectorResultDescriptor(sref, data_type, columns, time_bounds, spatial_bounds)
1✔
575

576
    @classmethod
1✔
577
    def is_vector_result(cls) -> bool:
1✔
578
        return True
1✔
579

580
    @property
1✔
581
    def data_type(self) -> VectorDataType:
1✔
582
        """Return the data type"""
583
        return self.__data_type
1✔
584

585
    @property
1✔
586
    def spatial_reference(self) -> str:
1✔
587
        """Return the spatial reference"""
588
        return super().spatial_reference
1✔
589

590
    @property
1✔
591
    def columns(self) -> dict[str, VectorColumnInfo]:
1✔
592
        """Return the columns"""
593

594
        return self.__columns
1✔
595

596
    @property
1✔
597
    def spatial_bounds(self) -> BoundingBox2D | None:
1✔
598
        """Return the spatial bounds"""
599
        return self.__spatial_bounds
1✔
600

601
    def __repr__(self) -> str:
1✔
602
        """Display representation of the vector result descriptor"""
603
        r = ""
1✔
604
        r += f"Data type:         {self.data_type.value}\n"
1✔
605
        r += f"Spatial Reference: {self.spatial_reference}\n"
1✔
606

607
        r += "Columns:\n"
1✔
608
        for column_name in self.columns:
1✔
609
            column_info = self.columns[column_name]
1✔
610
            r += f"  {column_name}:\n"
1✔
611
            r += f"    Column Type: {column_info.data_type.value}\n"
1✔
612
            r += f"    Measurement: {column_info.measurement}\n"
1✔
613

614
        return r
1✔
615

616
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
617
        """Convert the vector result descriptor to a dictionary"""
618

619
        return geoengine_openapi_client.TypedResultDescriptor(
1✔
620
            geoengine_openapi_client.TypedVectorResultDescriptor(
621
                type="vector",
622
                data_type=self.data_type.to_api_enum(),
623
                spatial_reference=self.spatial_reference,
624
                columns={name: column_info.to_api_dict() for name, column_info in self.columns.items()},
625
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
626
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
627
            )
628
        )
629

630

631
class FeatureDataType(str, Enum):
1✔
632
    """Vector column data type"""
633

634
    CATEGORY = "category"
1✔
635
    INT = "int"
1✔
636
    FLOAT = "float"
1✔
637
    TEXT = "text"
1✔
638
    BOOL = "bool"
1✔
639
    DATETIME = "dateTime"
1✔
640

641
    @staticmethod
1✔
642
    def from_string(data_type: str) -> FeatureDataType:
1✔
643
        """Create a new `VectorColumnDataType` from a string"""
644

645
        return FeatureDataType(data_type)
1✔
646

647
    def to_api_enum(self) -> geoengine_openapi_client.FeatureDataType:
1✔
648
        """Convert to an API enum"""
649

650
        return geoengine_openapi_client.FeatureDataType(self.value)
1✔
651

652

653
@dataclass
1✔
654
class VectorColumnInfo:
1✔
655
    """Vector column information"""
656

657
    data_type: FeatureDataType
1✔
658
    measurement: Measurement
1✔
659

660
    @staticmethod
1✔
661
    def from_response(response: geoengine_openapi_client.VectorColumnInfo) -> VectorColumnInfo:
1✔
662
        """Create a new `VectorColumnInfo` from a JSON response"""
663

664
        return VectorColumnInfo(
1✔
665
            FeatureDataType.from_string(response.data_type), Measurement.from_response(response.measurement)
666
        )
667

668
    def to_api_dict(self) -> geoengine_openapi_client.VectorColumnInfo:
1✔
669
        """Convert to a dictionary"""
670

671
        return geoengine_openapi_client.VectorColumnInfo(
1✔
672
            data_type=self.data_type.to_api_enum(),
673
            measurement=self.measurement.to_api_dict(),
674
        )
675

676

677
@dataclass(repr=False)
1✔
678
class RasterBandDescriptor:
1✔
679
    """A raster band descriptor"""
680

681
    name: str
1✔
682
    measurement: Measurement
1✔
683

684
    @classmethod
1✔
685
    def from_response(cls, response: geoengine_openapi_client.RasterBandDescriptor) -> RasterBandDescriptor:
1✔
686
        """Parse an http response to a `RasterBandDescriptor` object"""
687
        return RasterBandDescriptor(response.name, Measurement.from_response(response.measurement))
1✔
688

689
    def to_api_dict(self) -> geoengine_openapi_client.RasterBandDescriptor:
1✔
690
        return geoengine_openapi_client.RasterBandDescriptor(
1✔
691
            name=self.name,
692
            measurement=self.measurement.to_api_dict(),
693
        )
694

695
    def __repr__(self) -> str:
1✔
696
        """Display representation of a raster band descriptor"""
697
        return f"{self.name}: {self.measurement}"
×
698

699

700
@dataclass
1✔
701
class GridIdx2D:
1✔
702
    """A grid index"""
703

704
    x_idx: int
1✔
705
    y_idx: int
1✔
706

707
    @classmethod
1✔
708
    def from_response(cls, response: geoengine_openapi_client.GridIdx2D) -> GridIdx2D:
1✔
709
        """Parse an http response to a `GridIdx2D` object"""
710
        return GridIdx2D(x_idx=response.x_idx, y_idx=response.y_idx)
1✔
711

712
    def to_api_dict(self) -> geoengine_openapi_client.GridIdx2D:
1✔
713
        return geoengine_openapi_client.GridIdx2D(y_idx=self.y_idx, x_idx=self.x_idx)
1✔
714

715

716
@dataclass
1✔
717
class GridBoundingBox2D:
1✔
718
    """A grid boundingbox where lower right is inclusive index"""
719

720
    top_left_idx: GridIdx2D
1✔
721
    bottom_right_idx: GridIdx2D
1✔
722

723
    @classmethod
1✔
724
    def from_response(cls, response: geoengine_openapi_client.GridBoundingBox2D) -> GridBoundingBox2D:
1✔
725
        """Parse an http response to a `GridBoundingBox2D` object"""
726
        ul_idx = GridIdx2D.from_response(response.top_left_idx)
1✔
727
        lr_idx = GridIdx2D.from_response(response.bottom_right_idx)
1✔
728
        return GridBoundingBox2D(top_left_idx=ul_idx, bottom_right_idx=lr_idx)
1✔
729

730
    def to_api_dict(self) -> geoengine_openapi_client.GridBoundingBox2D:
1✔
731
        return geoengine_openapi_client.GridBoundingBox2D(
1✔
732
            top_left_idx=self.top_left_idx.to_api_dict(),
733
            bottom_right_idx=self.bottom_right_idx.to_api_dict(),
734
        )
735

736
    @property
1✔
737
    def width(self) -> int:
1✔
738
        return abs(self.bottom_right_idx.x_idx - self.top_left_idx.x_idx)
×
739

740
    @property
1✔
741
    def height(self) -> int:
1✔
742
        return abs(self.top_left_idx.y_idx - self.bottom_right_idx.y_idx)
×
743

744
    def contains_idx(self, idx: GridIdx2D) -> bool:
1✔
745
        """Test if a `GridIdx2D` is contained by this"""
746
        contains_x = self.top_left_idx.x_idx <= idx.x_idx <= self.bottom_right_idx.x_idx
×
747
        contains_y = self.top_left_idx.y_idx <= idx.y_idx <= self.bottom_right_idx.y_idx
×
748
        return contains_x and contains_y
×
749

750

751
@dataclass
1✔
752
class SpatialGridDefinition:
1✔
753
    """A grid boundingbox where lower right is inclusive index"""
754

755
    geo_transform: GeoTransform
1✔
756
    grid_bounds: GridBoundingBox2D
1✔
757

758
    @classmethod
1✔
759
    def from_response(cls, response: geoengine_openapi_client.SpatialGridDefinition) -> SpatialGridDefinition:
1✔
760
        """Parse an http response to a `SpatialGridDefinition` object"""
761
        geo_transform = GeoTransform.from_response(response.geo_transform)
1✔
762
        grid_bounds = GridBoundingBox2D.from_response(response.grid_bounds)
1✔
763
        return SpatialGridDefinition(geo_transform=geo_transform, grid_bounds=grid_bounds)
1✔
764

765
    def to_api_dict(self) -> geoengine_openapi_client.SpatialGridDefinition:
1✔
766
        return geoengine_openapi_client.SpatialGridDefinition(
1✔
767
            geo_transform=self.geo_transform.to_api_dict(),
768
            grid_bounds=self.grid_bounds.to_api_dict(),
769
        )
770

771
    def contains_idx(self, idx: GridIdx2D) -> bool:
1✔
772
        return self.grid_bounds.contains_idx(idx)
×
773

774
    def spatial_bounds(self) -> SpatialPartition2D:
1✔
775
        return self.geo_transform.grid_bounds_to_spatial_bounds(self.grid_bounds)
×
776

777
    def spatial_resolution(self) -> SpatialResolution:
1✔
778
        return self.geo_transform.spatial_resolution()
×
779

780
    def __repr__(self) -> str:
1✔
781
        """Display representation of the SpatialGridDefinition"""
782
        r = "SpatialGridDefinition: \n"
×
783
        r += f"    GeoTransform: {self.geo_transform}\n"
×
784
        r += f"    GridBounds: {self.grid_bounds}\n"
×
785
        return r
×
786

787

788
@dataclass
1✔
789
class SpatialGridDescriptor:
1✔
790
    """A grid boundingbox where lower right is inclusive index"""
791

792
    spatial_grid: SpatialGridDefinition
1✔
793
    descriptor: geoengine_openapi_client.SpatialGridDescriptorState
1✔
794

795
    @classmethod
1✔
796
    def from_response(cls, response: geoengine_openapi_client.SpatialGridDescriptor) -> SpatialGridDescriptor:
1✔
797
        """Parse an http response to a `SpatialGridDefinition` object"""
798
        spatial_grid = SpatialGridDefinition.from_response(response.spatial_grid)
1✔
799
        return SpatialGridDescriptor(spatial_grid=spatial_grid, descriptor=response.descriptor)
1✔
800

801
    def to_api_dict(self) -> geoengine_openapi_client.SpatialGridDescriptor:
1✔
802
        return geoengine_openapi_client.SpatialGridDescriptor(
1✔
803
            spatial_grid=self.spatial_grid.to_api_dict(),
804
            descriptor=self.descriptor,
805
        )
806

807
    def contains_idx(self, idx: GridIdx2D) -> bool:
1✔
808
        return self.spatial_grid.contains_idx(idx)
×
809

810
    def spatial_resolution(self) -> SpatialResolution:
1✔
811
        return self.spatial_grid.spatial_resolution()
×
812

813
    def spatial_bounds(self) -> SpatialPartition2D:
1✔
814
        return self.spatial_grid.spatial_bounds()
×
815

816
    def is_source(self) -> bool:
1✔
817
        return self.descriptor == "source"
×
818

819
    def is_derived(self) -> bool:
1✔
820
        return self.descriptor == "derived"
×
821

822
    def __repr__(self) -> str:
1✔
823
        """Display representation of the SpatialGridDescriptor"""
824
        r = "SpatialGridDescriptor: \n"
×
825
        r += f"    Definition: {self.spatial_grid}\n"
×
826
        r += f"    Is a {self.descriptor} grid.\n"
×
827
        return r
×
828

829

830
def literal_raster_data_type(
1✔
831
    data_type: geoengine_openapi_client.RasterDataType,
832
) -> Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]:
833
    """Convert a `RasterDataType` to a literal"""
834

835
    data_type_map: dict[
1✔
836
        geoengine_openapi_client.RasterDataType,
837
        Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
838
    ] = {
839
        geoengine_openapi_client.RasterDataType.U8: "U8",
840
        geoengine_openapi_client.RasterDataType.U16: "U16",
841
        geoengine_openapi_client.RasterDataType.U32: "U32",
842
        geoengine_openapi_client.RasterDataType.U64: "U64",
843
        geoengine_openapi_client.RasterDataType.I8: "I8",
844
        geoengine_openapi_client.RasterDataType.I16: "I16",
845
        geoengine_openapi_client.RasterDataType.I32: "I32",
846
        geoengine_openapi_client.RasterDataType.I64: "I64",
847
        geoengine_openapi_client.RasterDataType.F32: "F32",
848
        geoengine_openapi_client.RasterDataType.F64: "F64",
849
    }
850
    return data_type_map[data_type]
1✔
851

852

853
class TimeDimension:
1✔
854
    @classmethod
1✔
855
    def from_response(
1✔
856
        cls, response: geoengine_openapi_client.TimeDimension
857
    ) -> RegularTimeDimension | IrregularTimeDimension:
858
        actual = response.actual_instance
1✔
859

860
        if actual is None:
1✔
861
            raise ValueError("input is None")
×
862

863
        if actual.type == "regular":
1✔
864
            if not isinstance(actual, geoengine_openapi_client.RegularTimeDimension):
1✔
865
                raise ValueError("Type should be regular!")
×
866
            return RegularTimeDimension.from_response(response)
1✔
867

868
        if actual.type == "irregular":
1✔
869
            return IrregularTimeDimension.from_response(response)
1✔
870

871
        raise ValueError("unknown input type")
×
872

873
    @abstractmethod
1✔
874
    def to_api_dict(self) -> geoengine_openapi_client.TimeDimension:
1✔
875
        pass
×
876

877

878
class RegularTimeDimension(TimeDimension):
1✔
879
    """
880
    A regular time dimension
881
    """
882

883
    origin: np.datetime64
1✔
884
    step: TimeStep
1✔
885

886
    def __init__(self, step: TimeStep, origin: np.datetime64 | None = None) -> None:
1✔
887
        """Initialize a new `RegularTimeDimension`"""
888

889
        self.origin = origin if origin is not None else np.datetime64("1970-01-01T00:00:00Z")
1✔
890
        self.step = step
1✔
891

892
    def to_api_dict(self) -> geoengine_openapi_client.TimeDimension:
1✔
893
        """Convert the regular time dimension to a dictionary"""
894
        time_origin = self.origin.astype("datetime64[ms]").astype(int)
×
895
        return geoengine_openapi_client.TimeDimension(
×
896
            {"type": "regular", "origin": int(time_origin), "step": self.step.to_api_dict()}
897
        )
898

899
    @classmethod
1✔
900
    def from_response(cls, response: geoengine_openapi_client.TimeDimension) -> RegularTimeDimension:
1✔
901
        """Parse a regular time dimension from an http response"""
902

903
        actual = response.actual_instance
1✔
904

905
        if actual is None or actual.type != "regular":
1✔
906
            raise ValueError("type must be regular")
×
907

908
        if not isinstance(actual, geoengine_openapi_client.RegularTimeDimension):
1✔
909
            raise ValueError("Not a valid RegularTimeDimension")
×
910

911
        origin = np.datetime64(actual.origin, "ms")
1✔
912
        step = TimeStep.from_response(actual.step)
1✔
913
        return RegularTimeDimension(step=step, origin=origin)
1✔
914

915

916
class IrregularTimeDimension(TimeDimension):
1✔
917
    """The irregular time dimension"""
918

919
    def to_api_dict(self) -> geoengine_openapi_client.TimeDimension:
1✔
920
        """Convert the irregular time dimension to a dictionary"""
921

922
        return geoengine_openapi_client.TimeDimension({"type": "irregular"})
1✔
923

924
    @classmethod
1✔
925
    def from_response(cls, _response: Any) -> IrregularTimeDimension:
1✔
926
        """Parse an irregular time dimension from an http response"""
927
        return IrregularTimeDimension()
1✔
928

929

930
class TimeDescriptor:
1✔
931
    """A time descriptor"""
932

933
    bounds: TimeInterval | None
1✔
934
    dimension: TimeDimension
1✔
935

936
    def __init__(self, dimension: TimeDimension, bounds: TimeInterval | None = None) -> None:
1✔
937
        """Initialize a new `TimeDescriptor`"""
938
        self.dimension = dimension
1✔
939
        self.bounds = bounds
1✔
940

941
    def to_api_dict(self) -> geoengine_openapi_client.TimeDescriptor:
1✔
942
        """Convert the time descriptor to a dictionary"""
943
        return geoengine_openapi_client.TimeDescriptor(
1✔
944
            dimension=self.dimension.to_api_dict() if self.dimension is not None else None,
945
            bounds=self.bounds.to_api_dict() if self.bounds is not None else None,
946
        )
947

948
    @staticmethod
1✔
949
    def from_response(response: geoengine_openapi_client.TimeDescriptor) -> TimeDescriptor:
1✔
950
        """Parse a time descriptor from an http response"""
951
        bounds = None
1✔
952
        dimension = None
1✔
953

954
        if response.bounds is not None:
1✔
955
            bounds = TimeInterval.from_response(response.bounds)
1✔
956

957
        dimension = TimeDimension.from_response(response.dimension)
1✔
958

959
        return TimeDescriptor(bounds=bounds, dimension=dimension)
1✔
960

961

962
class RasterResultDescriptor(ResultDescriptor):
1✔
963
    """
964
    A raster result descriptor
965
    """
966

967
    __data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
968
    __bands: list[RasterBandDescriptor]
1✔
969
    __spatial_grid: SpatialGridDescriptor
1✔
970
    __time: TimeDescriptor
1✔
971

972
    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
1✔
973
        self,
974
        data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
975
        bands: list[RasterBandDescriptor],
976
        spatial_reference: str,
977
        spatial_grid: SpatialGridDescriptor,
978
        time: TimeDescriptor | TimeInterval | TimeDimension | None = None,
979
    ) -> None:
980
        """Initialize a new `RasterResultDescriptor`"""
981

982
        time_descriptor = time
1✔
983
        if isinstance(time, TimeInterval):
1✔
984
            time_descriptor = TimeDescriptor(dimension=IrregularTimeDimension(), bounds=time)
×
985
        elif isinstance(time, TimeDimension):
1✔
986
            time_descriptor = TimeDescriptor(dimension=time, bounds=None)
×
987

988
        if not isinstance(time_descriptor, TimeDescriptor):
1✔
989
            raise ValueError("no valid TimeDimension")
×
990

991
        super().__init__(spatial_reference, time_descriptor.bounds)
1✔
992
        self.__data_type = data_type
1✔
993
        self.__bands = bands
1✔
994
        self.__spatial_grid = spatial_grid
1✔
995
        self.__time = time_descriptor
1✔
996

997
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
998
        """Convert the raster result descriptor to a dictionary"""
999

1000
        return geoengine_openapi_client.TypedResultDescriptor(
1✔
1001
            geoengine_openapi_client.TypedRasterResultDescriptor(
1002
                type="raster",
1003
                data_type=self.data_type,
1004
                bands=[band.to_api_dict() for band in self.__bands],
1005
                spatial_reference=self.spatial_reference,
1006
                time=self.__time.to_api_dict(),
1007
                spatial_grid=self.__spatial_grid.to_api_dict(),
1008
            )
1009
        )
1010

1011
    @staticmethod
1✔
1012
    def from_response_raster(response: geoengine_openapi_client.TypedRasterResultDescriptor) -> RasterResultDescriptor:
1✔
1013
        """Parse a raster result descriptor from an http response"""
1014
        spatial_ref = response.spatial_reference
1✔
1015
        data_type = literal_raster_data_type(response.data_type)
1✔
1016
        bands = [RasterBandDescriptor.from_response(band) for band in response.bands]
1✔
1017

1018
        spatial_grid = SpatialGridDescriptor.from_response(response.spatial_grid)
1✔
1019

1020
        time_bounds = TimeDescriptor.from_response(response.time)
1✔
1021

1022
        return RasterResultDescriptor(
1✔
1023
            data_type=data_type,
1024
            bands=bands,
1025
            spatial_reference=spatial_ref,
1026
            time=time_bounds,
1027
            spatial_grid=spatial_grid,
1028
        )
1029

1030
    @classmethod
1✔
1031
    def is_raster_result(cls) -> bool:
1✔
1032
        return True
1✔
1033

1034
    @property
1✔
1035
    def data_type(self) -> Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]:
1✔
1036
        return self.__data_type
1✔
1037

1038
    @property
1✔
1039
    def bands(self) -> list[RasterBandDescriptor]:
1✔
1040
        return self.__bands
1✔
1041

1042
    @property
1✔
1043
    def spatial_grid(self) -> SpatialGridDescriptor:
1✔
1044
        return self.__spatial_grid
×
1045

1046
    @property
1✔
1047
    def spatial_bounds(self) -> SpatialPartition2D:
1✔
1048
        return self.spatial_grid.spatial_bounds()
×
1049

1050
    @property
1✔
1051
    def geo_transform(self) -> GeoTransform:
1✔
1052
        return self.spatial_grid.spatial_grid.geo_transform
×
1053

1054
    @property
1✔
1055
    def spatial_reference(self) -> str:
1✔
1056
        """Return the spatial reference"""
1057

1058
        return super().spatial_reference
1✔
1059

1060
    def __repr__(self) -> str:
1✔
1061
        """Display representation of the raster result descriptor"""
1062
        r = ""
×
1063
        r += f"Data type:         {self.data_type}\n"
×
1064
        r += f"Spatial Reference: {self.spatial_reference}\n"
×
1065
        r += f"Spatial Grid: {self.spatial_grid} \n"
×
1066
        r += f"Time Bounds: {self.time_bounds}\n"
×
1067
        r += "Bands:\n"
×
1068

1069
        for band in self.__bands:
×
1070
            r += f"    {band}\n"
×
1071

1072
        return r
×
1073

1074

1075
class PlotResultDescriptor(ResultDescriptor):
1✔
1076
    """
1077
    A plot result descriptor
1078
    """
1079

1080
    __spatial_bounds: BoundingBox2D | None
1✔
1081

1082
    def __init__(  # pylint: disable=too-many-arguments]
1✔
1083
        self,
1084
        spatial_reference: str,
1085
        time_bounds: TimeInterval | None = None,
1086
        spatial_bounds: BoundingBox2D | None = None,
1087
    ) -> None:
1088
        """Initialize a new `PlotResultDescriptor`"""
1089
        super().__init__(spatial_reference, time_bounds)
1✔
1090
        self.__spatial_bounds = spatial_bounds
1✔
1091

1092
    def __repr__(self) -> str:
1✔
1093
        """Display representation of the plot result descriptor"""
1094
        r = "Plot Result"
1✔
1095

1096
        return r
1✔
1097

1098
    @staticmethod
1✔
1099
    def from_response_plot(response: geoengine_openapi_client.TypedPlotResultDescriptor) -> PlotResultDescriptor:
1✔
1100
        """Create a new `PlotResultDescriptor` from a JSON response"""
1101
        spatial_ref = response.spatial_reference
1✔
1102

1103
        time_bounds = None
1✔
1104
        if response.time is not None:
1✔
1105
            time_bounds = TimeInterval.from_response(response.time)
1✔
1106
        spatial_bounds = None
1✔
1107
        if response.bbox is not None:
1✔
1108
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
1✔
1109

1110
        return PlotResultDescriptor(
1✔
1111
            spatial_reference=spatial_ref, time_bounds=time_bounds, spatial_bounds=spatial_bounds
1112
        )
1113

1114
    @classmethod
1✔
1115
    def is_plot_result(cls) -> bool:
1✔
1116
        return True
1✔
1117

1118
    @property
1✔
1119
    def spatial_reference(self) -> str:
1✔
1120
        """Return the spatial reference"""
1121
        return super().spatial_reference
×
1122

1123
    @property
1✔
1124
    def spatial_bounds(self) -> BoundingBox2D | None:
1✔
1125
        return self.__spatial_bounds
×
1126

1127
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
1128
        """Convert the plot result descriptor to a dictionary"""
1129

1130
        return geoengine_openapi_client.TypedResultDescriptor(
×
1131
            geoengine_openapi_client.TypedPlotResultDescriptor(
1132
                type="plot",
1133
                spatial_reference=self.spatial_reference,
1134
                data_type="Plot",
1135
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
1136
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
1137
            )
1138
        )
1139

1140

1141
class VectorDataType(str, Enum):
1✔
1142
    """An enum of vector data types"""
1143

1144
    DATA = "Data"
1✔
1145
    MULTI_POINT = "MultiPoint"
1✔
1146
    MULTI_LINE_STRING = "MultiLineString"
1✔
1147
    MULTI_POLYGON = "MultiPolygon"
1✔
1148

1149
    @classmethod
1✔
1150
    def from_geopandas_type_name(cls, name: str) -> VectorDataType:
1✔
1151
        """Resolve vector data type from geopandas geometry type"""
1152

1153
        name_map = {
1✔
1154
            "Point": VectorDataType.MULTI_POINT,
1155
            "MultiPoint": VectorDataType.MULTI_POINT,
1156
            "Line": VectorDataType.MULTI_LINE_STRING,
1157
            "MultiLine": VectorDataType.MULTI_LINE_STRING,
1158
            "Polygon": VectorDataType.MULTI_POLYGON,
1159
            "MultiPolygon": VectorDataType.MULTI_POLYGON,
1160
        }
1161

1162
        if name in name_map:
1✔
1163
            return name_map[name]
1✔
1164

1165
        raise InputException("Invalid vector data type")
×
1166

1167
    def to_api_enum(self) -> geoengine_openapi_client.VectorDataType:
1✔
1168
        return geoengine_openapi_client.VectorDataType(self.value)
1✔
1169

1170
    @staticmethod
1✔
1171
    def from_literal(literal: Literal["Data", "MultiPoint", "MultiLineString", "MultiPolygon"]) -> VectorDataType:
1✔
1172
        """Resolve vector data type from literal"""
1173
        return VectorDataType(literal)
×
1174

1175
    @staticmethod
1✔
1176
    def from_api_enum(data_type: geoengine_openapi_client.VectorDataType) -> VectorDataType:
1✔
1177
        """Resolve vector data type from API enum"""
1178
        return VectorDataType(data_type.value)
×
1179

1180
    @staticmethod
1✔
1181
    def from_string(string: str) -> VectorDataType:
1✔
1182
        """Resolve vector data type from string"""
1183
        if string not in VectorDataType.__members__.values():
1✔
1184
            raise InputException("Invalid vector data type: " + string)
×
1185
        return VectorDataType(string)
1✔
1186

1187

1188
class TimeStepGranularity(Enum):
1✔
1189
    """An enum of time step granularities"""
1190

1191
    MILLIS = "millis"
1✔
1192
    SECONDS = "seconds"
1✔
1193
    MINUTES = "minutes"
1✔
1194
    HOURS = "hours"
1✔
1195
    DAYS = "days"
1✔
1196
    MONTHS = "months"
1✔
1197
    YEARS = "years"
1✔
1198

1199
    def to_api_enum(self) -> geoengine_openapi_client.TimeGranularity:
1✔
1200
        return geoengine_openapi_client.TimeGranularity(self.value)
1✔
1201

1202

1203
@dataclass
1✔
1204
class TimeStep:
1✔
1205
    """A time step that consists of a granularity and a step size"""
1206

1207
    step: int
1✔
1208
    granularity: TimeStepGranularity
1✔
1209

1210
    def __init__(self, step: int, granularity: TimeStepGranularity | str) -> None:
1✔
1211
        """Initialize a new `TimeStep` object"""
1212
        self.step = step
×
1213
        if isinstance(granularity, str):
×
1214
            self.granularity = TimeStepGranularity(granularity)
×
1215
        else:
1216
            self.granularity = granularity
×
1217

1218
    def to_api_dict(self) -> geoengine_openapi_client.TimeStep:
1✔
1219
        return geoengine_openapi_client.TimeStep(
×
1220
            step=self.step,
1221
            granularity=self.granularity.to_api_enum(),
1222
        )
1223

1224
    @classmethod
1✔
1225
    def from_response(cls, response: geoengine_openapi_client.TimeStep) -> TimeStep:
1✔
1226
        """Parse an http response to a `TimeStep` object"""
1227
        granularity = TimeStepGranularity(response.granularity.value)
1✔
1228
        return TimeStep(step=response.step, granularity=granularity)
1✔
1229

1230

1231
@dataclass
1✔
1232
class Provenance:
1✔
1233
    """Provenance information as triplet of citation, license and uri"""
1234

1235
    citation: str
1✔
1236
    license: str
1✔
1237
    uri: str
1✔
1238

1239
    @classmethod
1✔
1240
    def from_response(cls, response: geoengine_openapi_client.Provenance) -> Provenance:
1✔
1241
        """Parse an http response to a `Provenance` object"""
1242
        return Provenance(response.citation, response.license, response.uri)
1✔
1243

1244
    def to_api_dict(self) -> geoengine_openapi_client.Provenance:
1✔
1245
        return geoengine_openapi_client.Provenance(
1✔
1246
            citation=self.citation,
1247
            license=self.license,
1248
            uri=self.uri,
1249
        )
1250

1251

1252
@dataclass
1✔
1253
class ProvenanceEntry:
1✔
1254
    """Provenance of a dataset"""
1255

1256
    data: list[DataId]
1✔
1257
    provenance: Provenance
1✔
1258

1259
    @classmethod
1✔
1260
    def from_response(cls, response: geoengine_openapi_client.ProvenanceEntry) -> ProvenanceEntry:
1✔
1261
        """Parse an http response to a `ProvenanceEntry` object"""
1262

1263
        dataset = [DataId.from_response(data) for data in response.data]
1✔
1264
        provenance = Provenance.from_response(response.provenance)
1✔
1265

1266
        return ProvenanceEntry(dataset, provenance)
1✔
1267

1268

1269
class Symbology:
1✔
1270
    """Base class for symbology"""
1271

1272
    @abstractmethod
1✔
1273
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1274
        pass
×
1275

1276
    @staticmethod
1✔
1277
    def from_response(response: geoengine_openapi_client.Symbology) -> Symbology:
1✔
1278
        """Parse an http response to a `Symbology` object"""
1279
        inner = response.actual_instance
1✔
1280

1281
        if isinstance(
1✔
1282
            inner,
1283
            geoengine_openapi_client.PointSymbology
1284
            | geoengine_openapi_client.LineSymbology
1285
            | geoengine_openapi_client.PolygonSymbology,
1286
        ):
1287
            # return VectorSymbology.from_response_vector(response)
1288
            return VectorSymbology()  # TODO: implement
×
1289
        if isinstance(inner, geoengine_openapi_client.RasterSymbology):
1✔
1290
            return RasterSymbology.from_response_raster(inner)
1✔
1291

1292
        raise InputException("Invalid symbology type")
×
1293

1294
    def __repr__(self):
1✔
1295
        "Symbology"
1296

1297

1298
class VectorSymbology(Symbology):
1✔
1299
    """A vector symbology"""
1300

1301
    # TODO: implement
1302

1303
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1304
        return None  # type: ignore
×
1305

1306

1307
class RasterColorizer:
1✔
1308
    """Base class for raster colorizer"""
1309

1310
    @classmethod
1✔
1311
    def from_response(cls, response: geoengine_openapi_client.RasterColorizer) -> RasterColorizer:
1✔
1312
        """Parse an http response to a `RasterColorizer` object"""
1313
        inner = response.actual_instance
1✔
1314

1315
        if isinstance(inner, geoengine_openapi_client.SingleBandRasterColorizer):
1✔
1316
            return SingleBandRasterColorizer.from_single_band_response(inner)
1✔
1317
        if isinstance(inner, geoengine_openapi_client.MultiBandRasterColorizer):
1✔
1318
            return MultiBandRasterColorizer.from_multi_band_response(inner)
1✔
1319

1320
        raise GeoEngineException({"message": "Unknown RasterColorizer type"})
×
1321

1322
    @abstractmethod
1✔
1323
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1324
        pass
×
1325

1326

1327
@dataclass
1✔
1328
class SingleBandRasterColorizer(RasterColorizer):
1✔
1329
    """A raster colorizer for a specified band"""
1330

1331
    band: int
1✔
1332
    band_colorizer: Colorizer
1✔
1333

1334
    @staticmethod
1✔
1335
    def from_single_band_response(response: geoengine_openapi_client.SingleBandRasterColorizer) -> RasterColorizer:
1✔
1336
        return SingleBandRasterColorizer(response.band, Colorizer.from_response(response.band_colorizer))
1✔
1337

1338
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1339
        return geoengine_openapi_client.RasterColorizer(
1✔
1340
            geoengine_openapi_client.SingleBandRasterColorizer(
1341
                type="singleBand",
1342
                band=self.band,
1343
                band_colorizer=self.band_colorizer.to_api_dict(),
1344
            )
1345
        )
1346

1347

1348
@dataclass
1✔
1349
class MultiBandRasterColorizer(RasterColorizer):
1✔
1350
    """A raster colorizer for multiple bands"""
1351

1352
    blue_band: int
1✔
1353
    blue_max: float
1✔
1354
    blue_min: float
1✔
1355
    blue_scale: float | None
1✔
1356
    green_band: int
1✔
1357
    green_max: float
1✔
1358
    green_min: float
1✔
1359
    green_scale: float | None
1✔
1360
    red_band: int
1✔
1361
    red_max: float
1✔
1362
    red_min: float
1✔
1363
    red_scale: float | None
1✔
1364

1365
    @staticmethod
1✔
1366
    def from_multi_band_response(response: geoengine_openapi_client.MultiBandRasterColorizer) -> RasterColorizer:
1✔
1367
        return MultiBandRasterColorizer(
1✔
1368
            response.blue_band,
1369
            response.blue_max,
1370
            response.blue_min,
1371
            response.blue_scale,
1372
            response.green_band,
1373
            response.green_max,
1374
            response.green_min,
1375
            response.green_scale,
1376
            response.red_band,
1377
            response.red_max,
1378
            response.red_min,
1379
            response.red_scale,
1380
        )
1381

1382
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1383
        return geoengine_openapi_client.RasterColorizer(
1✔
1384
            geoengine_openapi_client.MultiBandRasterColorizer(
1385
                type="multiBand",
1386
                blue_band=self.blue_band,
1387
                blue_max=self.blue_max,
1388
                blue_min=self.blue_min,
1389
                blue_scale=self.blue_scale,
1390
                green_band=self.green_band,
1391
                green_max=self.green_max,
1392
                green_min=self.green_min,
1393
                green_scale=self.green_scale,
1394
                red_band=self.red_band,
1395
                red_max=self.red_max,
1396
                red_min=self.red_min,
1397
                red_scale=self.red_scale,
1398
            )
1399
        )
1400

1401

1402
class RasterSymbology(Symbology):
1✔
1403
    """A raster symbology"""
1404

1405
    opacity: float
1✔
1406
    raster_colorizer: RasterColorizer
1✔
1407

1408
    def __init__(self, raster_colorizer: RasterColorizer, opacity: float = 1.0) -> None:
1✔
1409
        """Initialize a new `RasterSymbology`"""
1410

1411
        self.raster_colorizer = raster_colorizer
1✔
1412
        self.opacity = opacity
1✔
1413

1414
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1415
        """Convert the raster symbology to a dictionary"""
1416

1417
        return geoengine_openapi_client.Symbology(
1✔
1418
            geoengine_openapi_client.RasterSymbology(
1419
                type="raster",
1420
                raster_colorizer=self.raster_colorizer.to_api_dict(),
1421
                opacity=self.opacity,
1422
            )
1423
        )
1424

1425
    @staticmethod
1✔
1426
    def from_response_raster(response: geoengine_openapi_client.RasterSymbology) -> RasterSymbology:
1✔
1427
        """Parse an http response to a `RasterSymbology` object"""
1428

1429
        raster_colorizer = RasterColorizer.from_response(response.raster_colorizer)
1✔
1430

1431
        return RasterSymbology(raster_colorizer, response.opacity)
1✔
1432

1433
    def __repr__(self) -> str:
1✔
1434
        return str(self.__class__) + f"({self.raster_colorizer}, {self.opacity})"
×
1435

1436
    def __eq__(self, value):
1✔
1437
        """Check if two RasterSymbologies are equal"""
1438

1439
        if not isinstance(value, self.__class__):
1✔
1440
            return False
×
1441
        return self.opacity == value.opacity and self.raster_colorizer == value.raster_colorizer
1✔
1442

1443

1444
class DataId:  # pylint: disable=too-few-public-methods
1✔
1445
    """Base class for data ids"""
1446

1447
    @classmethod
1✔
1448
    def from_response(cls, response: geoengine_openapi_client.DataId) -> DataId:
1✔
1449
        """Parse an http response to a `DataId` object"""
1450
        inner = response.actual_instance
1✔
1451

1452
        if isinstance(inner, geoengine_openapi_client.InternalDataId):
1✔
1453
            return InternalDataId.from_response_internal(inner)
1✔
1454
        if isinstance(inner, geoengine_openapi_client.ExternalDataId):
×
1455
            return ExternalDataId.from_response_external(inner)
×
1456

1457
        raise GeoEngineException({"message": "Unknown DataId type"})
×
1458

1459
    @abstractmethod
1✔
1460
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1461
        pass
×
1462

1463

1464
class InternalDataId(DataId):
1✔
1465
    """An internal data id"""
1466

1467
    __dataset_id: UUID
1✔
1468

1469
    def __init__(self, dataset_id: UUID):
1✔
1470
        self.__dataset_id = dataset_id
1✔
1471

1472
    @classmethod
1✔
1473
    def from_response_internal(cls, response: geoengine_openapi_client.InternalDataId) -> InternalDataId:
1✔
1474
        """Parse an http response to a `InternalDataId` object"""
1475
        return InternalDataId(response.dataset_id)
1✔
1476

1477
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1478
        return geoengine_openapi_client.DataId(
×
1479
            geoengine_openapi_client.InternalDataId(type="internal", dataset_id=str(self.__dataset_id))
1480
        )
1481

1482
    def __str__(self) -> str:
1✔
1483
        return str(self.__dataset_id)
×
1484

1485
    def __repr__(self) -> str:
1✔
1486
        """Display representation of an internal data id"""
1487
        return str(self)
×
1488

1489
    def __eq__(self, other) -> bool:
1✔
1490
        """Check if two internal data ids are equal"""
1491
        if not isinstance(other, self.__class__):
1✔
1492
            return False
×
1493

1494
        return self.__dataset_id == other.__dataset_id  # pylint: disable=protected-access
1✔
1495

1496

1497
class ExternalDataId(DataId):
1✔
1498
    """An external data id"""
1499

1500
    __provider_id: UUID
1✔
1501
    __layer_id: str
1✔
1502

1503
    def __init__(self, provider_id: UUID, layer_id: str):
1✔
1504
        self.__provider_id = provider_id
×
1505
        self.__layer_id = layer_id
×
1506

1507
    @classmethod
1✔
1508
    def from_response_external(cls, response: geoengine_openapi_client.ExternalDataId) -> ExternalDataId:
1✔
1509
        """Parse an http response to a `ExternalDataId` object"""
1510

1511
        return ExternalDataId(response.provider_id, response.layer_id)
×
1512

1513
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1514
        return geoengine_openapi_client.DataId(
×
1515
            geoengine_openapi_client.ExternalDataId(
1516
                type="external",
1517
                provider_id=str(self.__provider_id),
1518
                layer_id=self.__layer_id,
1519
            )
1520
        )
1521

1522
    def __str__(self) -> str:
1✔
1523
        return f"{self.__provider_id}:{self.__layer_id}"
×
1524

1525
    def __repr__(self) -> str:
1✔
1526
        """Display representation of an external data id"""
1527
        return str(self)
×
1528

1529
    def __eq__(self, other) -> bool:
1✔
1530
        """Check if two external data ids are equal"""
1531
        if not isinstance(other, self.__class__):
×
1532
            return False
×
1533

1534
        return self.__provider_id == other.__provider_id and self.__layer_id == other.__layer_id  # pylint: disable=protected-access
×
1535

1536

1537
class Measurement:  # pylint: disable=too-few-public-methods
1✔
1538
    """
1539
    Base class for measurements
1540
    """
1541

1542
    @staticmethod
1✔
1543
    def from_response(response: geoengine_openapi_client.Measurement) -> Measurement:
1✔
1544
        """
1545
        Parse a result descriptor from an http response
1546
        """
1547
        inner = response.actual_instance
1✔
1548

1549
        if isinstance(inner, geoengine_openapi_client.UnitlessMeasurement):
1✔
1550
            return UnitlessMeasurement()
1✔
1551
        if isinstance(inner, geoengine_openapi_client.ContinuousMeasurement):
1✔
1552
            return ContinuousMeasurement.from_response_continuous(inner)
1✔
1553
        if isinstance(inner, geoengine_openapi_client.ClassificationMeasurement):
1✔
1554
            return ClassificationMeasurement.from_response_classification(inner)
1✔
1555

1556
        raise TypeException("Unknown `Measurement` type")
×
1557

1558
    @abstractmethod
1✔
1559
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1560
        pass
×
1561

1562

1563
class UnitlessMeasurement(Measurement):
1✔
1564
    """A measurement that is unitless"""
1565

1566
    def __str__(self) -> str:
1✔
1567
        """String representation of a unitless measurement"""
1568
        return "unitless"
1✔
1569

1570
    def __repr__(self) -> str:
1✔
1571
        """Display representation of a unitless measurement"""
1572
        return str(self)
×
1573

1574
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1575
        return geoengine_openapi_client.Measurement(geoengine_openapi_client.UnitlessMeasurement(type="unitless"))
1✔
1576

1577

1578
class ContinuousMeasurement(Measurement):
1✔
1579
    """A measurement that is continuous"""
1580

1581
    __measurement: str
1✔
1582
    __unit: str | None
1✔
1583

1584
    def __init__(self, measurement: str, unit: str | None) -> None:
1✔
1585
        """Initialize a new `ContiuousMeasurement`"""
1586

1587
        super().__init__()
1✔
1588

1589
        self.__measurement = measurement
1✔
1590
        self.__unit = unit
1✔
1591

1592
    @staticmethod
1✔
1593
    def from_response_continuous(response: geoengine_openapi_client.ContinuousMeasurement) -> ContinuousMeasurement:
1✔
1594
        """Initialize a new `ContiuousMeasurement from a JSON response"""
1595

1596
        return ContinuousMeasurement(response.measurement, response.unit)
1✔
1597

1598
    def __str__(self) -> str:
1✔
1599
        """String representation of a continuous measurement"""
1600

1601
        if self.__unit is None:
1✔
1602
            return self.__measurement
1✔
1603

1604
        return f"{self.__measurement} ({self.__unit})"
×
1605

1606
    def __repr__(self) -> str:
1✔
1607
        """Display representation of a continuous measurement"""
1608
        return str(self)
×
1609

1610
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1611
        return geoengine_openapi_client.Measurement(
1✔
1612
            geoengine_openapi_client.ContinuousMeasurement(
1613
                type="continuous", measurement=self.__measurement, unit=self.__unit
1614
            )
1615
        )
1616

1617
    @property
1✔
1618
    def measurement(self) -> str:
1✔
1619
        return self.__measurement
×
1620

1621
    @property
1✔
1622
    def unit(self) -> str | None:
1✔
1623
        return self.__unit
×
1624

1625

1626
class ClassificationMeasurement(Measurement):
1✔
1627
    """A measurement that is a classification"""
1628

1629
    __measurement: str
1✔
1630
    __classes: dict[int, str]
1✔
1631

1632
    def __init__(self, measurement: str, classes: dict[int, str]) -> None:
1✔
1633
        """Initialize a new `ClassificationMeasurement`"""
1634

1635
        super().__init__()
1✔
1636

1637
        self.__measurement = measurement
1✔
1638
        self.__classes = classes
1✔
1639

1640
    @staticmethod
1✔
1641
    def from_response_classification(
1✔
1642
        response: geoengine_openapi_client.ClassificationMeasurement,
1643
    ) -> ClassificationMeasurement:
1644
        """Initialize a new `ClassificationMeasurement from a JSON response"""
1645

1646
        measurement = response.measurement
1✔
1647

1648
        str_classes: dict[str, str] = response.classes
1✔
1649
        classes = {int(k): v for k, v in str_classes.items()}
1✔
1650

1651
        return ClassificationMeasurement(measurement, classes)
1✔
1652

1653
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1654
        str_classes: dict[str, str] = {str(k): v for k, v in self.__classes.items()}
1✔
1655

1656
        return geoengine_openapi_client.Measurement(
1✔
1657
            geoengine_openapi_client.ClassificationMeasurement(
1658
                type="classification", measurement=self.__measurement, classes=str_classes
1659
            )
1660
        )
1661

1662
    def __str__(self) -> str:
1✔
1663
        """String representation of a classification measurement"""
1664
        classes_str = ", ".join(f"{k}: {v}" for k, v in self.__classes.items())
×
1665
        return f"{self.__measurement} ({classes_str})"
×
1666

1667
    def __repr__(self) -> str:
1✔
1668
        """Display representation of a classification measurement"""
1669
        return str(self)
×
1670

1671
    @property
1✔
1672
    def measurement(self) -> str:
1✔
1673
        return self.__measurement
×
1674

1675
    @property
1✔
1676
    def classes(self) -> dict[int, str]:
1✔
1677
        return self.__classes
×
1678

1679

1680
class GeoTransform:
1✔
1681
    """The `GeoTransform` specifies the relationship between pixel coordinates and geographic coordinates."""
1682

1683
    x_min: float
1✔
1684
    y_max: float
1✔
1685
    """In Geo Engine, x_pixel_size is always positive."""
1✔
1686
    x_pixel_size: float
1✔
1687
    """In Geo Engine, y_pixel_size is always negative."""
1✔
1688
    y_pixel_size: float
1✔
1689

1690
    def __init__(self, x_min: float, y_max: float, x_pixel_size: float, y_pixel_size: float):
1✔
1691
        """Initialize a new `GeoTransform`"""
1692

1693
        assert x_pixel_size > 0, "In Geo Engine, x_pixel_size is always positive."
1✔
1694
        assert y_pixel_size < 0, "In Geo Engine, y_pixel_size is always negative."
1✔
1695

1696
        self.x_min = x_min
1✔
1697
        self.y_max = y_max
1✔
1698
        self.x_pixel_size = x_pixel_size
1✔
1699
        self.y_pixel_size = y_pixel_size
1✔
1700

1701
    @classmethod
1✔
1702
    def from_response_gdal_geo_transform(
1✔
1703
        cls, response: geoengine_openapi_client.GdalDatasetGeoTransform
1704
    ) -> GeoTransform:
1705
        """Parse a geotransform from an HTTP JSON response"""
1706
        return GeoTransform(
×
1707
            x_min=response.origin_coordinate.x,
1708
            y_max=response.origin_coordinate.y,
1709
            x_pixel_size=response.x_pixel_size,
1710
            y_pixel_size=response.y_pixel_size,
1711
        )
1712

1713
    @classmethod
1✔
1714
    def from_response(cls, response: geoengine_openapi_client.GeoTransform) -> GeoTransform:
1✔
1715
        """Parse a geotransform from an HTTP JSON response"""
1716

1717
        return GeoTransform(
1✔
1718
            x_min=response.origin_coordinate.x,
1719
            y_max=response.origin_coordinate.y,
1720
            x_pixel_size=response.x_pixel_size,
1721
            y_pixel_size=response.y_pixel_size,
1722
        )
1723

1724
    def to_api_dict(self) -> geoengine_openapi_client.GeoTransform:
1✔
1725
        return geoengine_openapi_client.GeoTransform(
1✔
1726
            origin_coordinate=geoengine_openapi_client.Coordinate2D(
1727
                x=self.x_min,
1728
                y=self.y_max,
1729
            ),
1730
            x_pixel_size=self.x_pixel_size,
1731
            y_pixel_size=self.y_pixel_size,
1732
        )
1733

1734
    def to_api_dict_gdal_geo_transform(self) -> geoengine_openapi_client.GdalDatasetGeoTransform:
1✔
1735
        return geoengine_openapi_client.GdalDatasetGeoTransform(
×
1736
            origin_coordinate=geoengine_openapi_client.Coordinate2D(
1737
                x=self.x_min,
1738
                y=self.y_max,
1739
            ),
1740
            x_pixel_size=self.x_pixel_size,
1741
            y_pixel_size=self.y_pixel_size,
1742
        )
1743

1744
    def to_gdal(self) -> tuple[float, float, float, float, float, float]:
1✔
1745
        """Convert to a GDAL geotransform"""
1746
        return (self.x_min, self.x_pixel_size, 0, self.y_max, 0, self.y_pixel_size)
×
1747

1748
    def __str__(self) -> str:
1✔
1749
        return (
×
1750
            f"Origin: ({self.x_min}, {self.y_max}), "
1751
            f"X Pixel Size: {self.x_pixel_size}, "
1752
            f"Y Pixel Size: {self.y_pixel_size}"
1753
        )
1754

1755
    def __repr__(self) -> str:
1✔
1756
        return str(self)
×
1757

1758
    @property
1✔
1759
    def x_half_pixel_size(self) -> float:
1✔
1760
        return self.x_pixel_size / 2.0
1✔
1761

1762
    @property
1✔
1763
    def y_half_pixel_size(self) -> float:
1✔
1764
        return self.y_pixel_size / 2.0
1✔
1765

1766
    def pixel_x_to_coord_x(self, pixel: int) -> float:
1✔
1767
        return self.x_min + pixel * self.x_pixel_size
1✔
1768

1769
    def pixel_y_to_coord_y(self, pixel: int) -> float:
1✔
1770
        return self.y_max + pixel * self.y_pixel_size
1✔
1771

1772
    def coord_to_pixel_ul(self, x_cord: float, y_coord: float) -> GridIdx2D:
1✔
1773
        """Convert a coordinate to a pixel index rould towards top left"""
1774
        return GridIdx2D(
×
1775
            x_idx=int(np.floor((x_cord - self.x_min) / self.x_pixel_size)),
1776
            y_idx=int(np.ceil((y_coord - self.y_max) / self.y_pixel_size)),
1777
        )
1778

1779
    def coord_to_pixel_lr(self, x_cord: float, y_coord: float) -> GridIdx2D:
1✔
1780
        """Convert a coordinate to a pixel index ound towards lower right"""
1781
        return GridIdx2D(
×
1782
            x_idx=int(np.ceil((x_cord - self.x_min) / self.x_pixel_size)),
1783
            y_idx=int(np.floor((y_coord - self.y_max) / self.y_pixel_size)),
1784
        )
1785

1786
    def pixel_ul_to_coord(self, x_pixel: int, y_pixel: int) -> tuple[float, float]:
1✔
1787
        """Convert a pixel position into a coordinate"""
1788
        x = self.pixel_x_to_coord_x(x_pixel)
×
1789
        y = self.pixel_y_to_coord_y(y_pixel)
×
1790
        return (x, y)
×
1791

1792
    def pixel_lr_to_coord(self, x_pixel: int, y_pixel: int) -> tuple[float, float]:
1✔
1793
        (x, y) = self.pixel_ul_to_coord(x_pixel, y_pixel)
×
1794
        return (x + self.x_pixel_size, y + self.y_pixel_size)
×
1795

1796
    def pixel_center_to_coord(self, x_pixel, y_pixel) -> tuple[float, float]:
1✔
1797
        (x, y) = self.pixel_ul_to_coord(x_pixel, y_pixel)
×
1798
        return (x + self.x_half_pixel_size, y + self.y_half_pixel_size)
×
1799

1800
    def spatial_resolution(self) -> SpatialResolution:
1✔
1801
        return SpatialResolution(x_resolution=abs(self.x_pixel_size), y_resolution=abs(self.y_pixel_size))
×
1802

1803
    def spatial_to_grid_bounds(self, bounds: SpatialPartition2D | BoundingBox2D) -> GridBoundingBox2D:
1✔
1804
        """Converts a BoundingBox2D or a SpatialPartition2D into a GridBoundingBox2D"""
1805
        ul = self.coord_to_pixel_ul(bounds.xmin, bounds.ymax)
×
1806
        rl = self.coord_to_pixel_lr(bounds.xmax, bounds.ymin)
×
1807
        return GridBoundingBox2D(top_left_idx=ul, bottom_right_idx=rl)
×
1808

1809
    def grid_bounds_to_spatial_bounds(self, bounds: GridBoundingBox2D) -> SpatialPartition2D:
1✔
1810
        """Converts a GridBoundingBox2D into a SpatialPartition2D"""
1811
        xmin, ymax = self.pixel_ul_to_coord(bounds.top_left_idx.x_idx, bounds.top_left_idx.y_idx)
×
1812
        xmax, ymin = self.pixel_lr_to_coord(bounds.bottom_right_idx.x_idx, bounds.bottom_right_idx.y_idx)
×
1813
        return SpatialPartition2D(xmin, ymin, xmax, ymax)
×
1814

1815
    def __eq__(self, other) -> bool:
1✔
1816
        """Check if two geotransforms are equal"""
1817
        if not isinstance(other, GeoTransform):
×
1818
            return False
×
1819

1820
        return (
×
1821
            self.x_min == other.x_min
1822
            and self.y_max == other.y_max
1823
            and self.x_pixel_size == other.x_pixel_size
1824
            and self.y_pixel_size == other.y_pixel_size
1825
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc