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

geo-engine / geoengine-python / 17064361033

19 Aug 2025 08:43AM UTC coverage: 76.088% (-0.9%) from 76.961%
17064361033

Pull #221

github

web-flow
Merge 78613fd6d into 798243b77
Pull Request #221: Pixel_based_queries_rewrite

2921 of 3839 relevant lines covered (76.09%)

0.76 hits per line

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

80.47
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(
×
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)
×
570
        spatial_bounds = None
1✔
571
        if response.bbox is not None:
1✔
572
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
×
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 RasterResultDescriptor(ResultDescriptor):
1✔
854
    """
855
    A raster result descriptor
856
    """
857

858
    __data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
859
    __bands: list[RasterBandDescriptor]
1✔
860
    __spatial_grid: SpatialGridDescriptor
1✔
861

862
    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
1✔
863
        self,
864
        data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
865
        bands: list[RasterBandDescriptor],
866
        spatial_reference: str,
867
        spatial_grid: SpatialGridDescriptor,
868
        time_bounds: TimeInterval | None = None,
869
    ) -> None:
870
        """Initialize a new `RasterResultDescriptor`"""
871
        super().__init__(spatial_reference, time_bounds)
1✔
872
        self.__data_type = data_type
1✔
873
        self.__bands = bands
1✔
874
        self.__spatial_grid = spatial_grid
1✔
875

876
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
877
        """Convert the raster result descriptor to a dictionary"""
878

879
        return geoengine_openapi_client.TypedResultDescriptor(
1✔
880
            geoengine_openapi_client.TypedRasterResultDescriptor(
881
                type="raster",
882
                data_type=self.data_type,
883
                bands=[band.to_api_dict() for band in self.__bands],
884
                spatial_reference=self.spatial_reference,
885
                time=self.time_bounds.time_str if self.time_bounds is not None else None,
886
                spatial_grid=self.__spatial_grid.to_api_dict(),
887
            )
888
        )
889

890
    @staticmethod
1✔
891
    def from_response_raster(response: geoengine_openapi_client.TypedRasterResultDescriptor) -> RasterResultDescriptor:
1✔
892
        """Parse a raster result descriptor from an http response"""
893
        spatial_ref = response.spatial_reference
1✔
894
        data_type = literal_raster_data_type(response.data_type)
1✔
895
        bands = [RasterBandDescriptor.from_response(band) for band in response.bands]
1✔
896

897
        time_bounds = None
1✔
898

899
        # FIXME: datetime can not represent our min max range
900
        # if 'time' in response and response['time'] is not None:
901
        #    time_bounds = TimeInterval.from_response(response['time'])
902
        spatial_grid = SpatialGridDescriptor.from_response(response.spatial_grid)
1✔
903

904
        if response.time is not None:
1✔
905
            time_bounds = TimeInterval.from_response(response.time)
×
906

907
        return RasterResultDescriptor(
1✔
908
            data_type=data_type,
909
            bands=bands,
910
            spatial_reference=spatial_ref,
911
            time_bounds=time_bounds,
912
            spatial_grid=spatial_grid,
913
        )
914

915
    @classmethod
1✔
916
    def is_raster_result(cls) -> bool:
1✔
917
        return True
1✔
918

919
    @property
1✔
920
    def data_type(self) -> Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]:
1✔
921
        return self.__data_type
1✔
922

923
    @property
1✔
924
    def bands(self) -> list[RasterBandDescriptor]:
1✔
925
        return self.__bands
1✔
926

927
    @property
1✔
928
    def spatial_grid(self) -> SpatialGridDescriptor:
1✔
929
        return self.__spatial_grid
×
930

931
    @property
1✔
932
    def spatial_bounds(self) -> SpatialPartition2D:
1✔
933
        return self.spatial_grid.spatial_bounds()
×
934

935
    @property
1✔
936
    def geo_transform(self) -> GeoTransform:
1✔
937
        return self.spatial_grid.spatial_grid.geo_transform
×
938

939
    @property
1✔
940
    def spatial_reference(self) -> str:
1✔
941
        """Return the spatial reference"""
942

943
        return super().spatial_reference
1✔
944

945
    def __repr__(self) -> str:
1✔
946
        """Display representation of the raster result descriptor"""
947
        r = ""
×
948
        r += f"Data type:         {self.data_type}\n"
×
949
        r += f"Spatial Reference: {self.spatial_reference}\n"
×
950
        r += f"Spatial Grid: {self.spatial_grid} \n"
×
951
        r += f"Time Bounds: {self.time_bounds}\n"
×
952
        r += "Bands:\n"
×
953

954
        for band in self.__bands:
×
955
            r += f"    {band}\n"
×
956

957
        return r
×
958

959

960
class PlotResultDescriptor(ResultDescriptor):
1✔
961
    """
962
    A plot result descriptor
963
    """
964

965
    __spatial_bounds: BoundingBox2D | None
1✔
966

967
    def __init__(  # pylint: disable=too-many-arguments]
1✔
968
        self,
969
        spatial_reference: str,
970
        time_bounds: TimeInterval | None = None,
971
        spatial_bounds: BoundingBox2D | None = None,
972
    ) -> None:
973
        """Initialize a new `PlotResultDescriptor`"""
974
        super().__init__(spatial_reference, time_bounds)
1✔
975
        self.__spatial_bounds = spatial_bounds
1✔
976

977
    def __repr__(self) -> str:
1✔
978
        """Display representation of the plot result descriptor"""
979
        r = "Plot Result"
1✔
980

981
        return r
1✔
982

983
    @staticmethod
1✔
984
    def from_response_plot(response: geoengine_openapi_client.TypedPlotResultDescriptor) -> PlotResultDescriptor:
1✔
985
        """Create a new `PlotResultDescriptor` from a JSON response"""
986
        spatial_ref = response.spatial_reference
1✔
987

988
        time_bounds = None
1✔
989
        if response.time is not None:
1✔
990
            time_bounds = TimeInterval.from_response(response.time)
1✔
991
        spatial_bounds = None
1✔
992
        if response.bbox is not None:
1✔
993
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
1✔
994

995
        return PlotResultDescriptor(
1✔
996
            spatial_reference=spatial_ref, time_bounds=time_bounds, spatial_bounds=spatial_bounds
997
        )
998

999
    @classmethod
1✔
1000
    def is_plot_result(cls) -> bool:
1✔
1001
        return True
1✔
1002

1003
    @property
1✔
1004
    def spatial_reference(self) -> str:
1✔
1005
        """Return the spatial reference"""
1006
        return super().spatial_reference
×
1007

1008
    @property
1✔
1009
    def spatial_bounds(self) -> BoundingBox2D | None:
1✔
1010
        return self.__spatial_bounds
×
1011

1012
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
1013
        """Convert the plot result descriptor to a dictionary"""
1014

1015
        return geoengine_openapi_client.TypedResultDescriptor(
×
1016
            geoengine_openapi_client.TypedPlotResultDescriptor(
1017
                type="plot",
1018
                spatial_reference=self.spatial_reference,
1019
                data_type="Plot",
1020
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
1021
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
1022
            )
1023
        )
1024

1025

1026
class VectorDataType(str, Enum):
1✔
1027
    """An enum of vector data types"""
1028

1029
    DATA = "Data"
1✔
1030
    MULTI_POINT = "MultiPoint"
1✔
1031
    MULTI_LINE_STRING = "MultiLineString"
1✔
1032
    MULTI_POLYGON = "MultiPolygon"
1✔
1033

1034
    @classmethod
1✔
1035
    def from_geopandas_type_name(cls, name: str) -> VectorDataType:
1✔
1036
        """Resolve vector data type from geopandas geometry type"""
1037

1038
        name_map = {
1✔
1039
            "Point": VectorDataType.MULTI_POINT,
1040
            "MultiPoint": VectorDataType.MULTI_POINT,
1041
            "Line": VectorDataType.MULTI_LINE_STRING,
1042
            "MultiLine": VectorDataType.MULTI_LINE_STRING,
1043
            "Polygon": VectorDataType.MULTI_POLYGON,
1044
            "MultiPolygon": VectorDataType.MULTI_POLYGON,
1045
        }
1046

1047
        if name in name_map:
1✔
1048
            return name_map[name]
1✔
1049

1050
        raise InputException("Invalid vector data type")
×
1051

1052
    def to_api_enum(self) -> geoengine_openapi_client.VectorDataType:
1✔
1053
        return geoengine_openapi_client.VectorDataType(self.value)
1✔
1054

1055
    @staticmethod
1✔
1056
    def from_literal(literal: Literal["Data", "MultiPoint", "MultiLineString", "MultiPolygon"]) -> VectorDataType:
1✔
1057
        """Resolve vector data type from literal"""
1058
        return VectorDataType(literal)
×
1059

1060
    @staticmethod
1✔
1061
    def from_api_enum(data_type: geoengine_openapi_client.VectorDataType) -> VectorDataType:
1✔
1062
        """Resolve vector data type from API enum"""
1063
        return VectorDataType(data_type.value)
×
1064

1065
    @staticmethod
1✔
1066
    def from_string(string: str) -> VectorDataType:
1✔
1067
        """Resolve vector data type from string"""
1068
        if string not in VectorDataType.__members__.values():
1✔
1069
            raise InputException("Invalid vector data type: " + string)
×
1070
        return VectorDataType(string)
1✔
1071

1072

1073
class TimeStepGranularity(Enum):
1✔
1074
    """An enum of time step granularities"""
1075

1076
    MILLIS = "millis"
1✔
1077
    SECONDS = "seconds"
1✔
1078
    MINUTES = "minutes"
1✔
1079
    HOURS = "hours"
1✔
1080
    DAYS = "days"
1✔
1081
    MONTHS = "months"
1✔
1082
    YEARS = "years"
1✔
1083

1084
    def to_api_enum(self) -> geoengine_openapi_client.TimeGranularity:
1✔
1085
        return geoengine_openapi_client.TimeGranularity(self.value)
1✔
1086

1087

1088
@dataclass
1✔
1089
class TimeStep:
1✔
1090
    """A time step that consists of a granularity and a step size"""
1091

1092
    step: int
1✔
1093
    granularity: TimeStepGranularity
1✔
1094

1095
    def to_api_dict(self) -> geoengine_openapi_client.TimeStep:
1✔
1096
        return geoengine_openapi_client.TimeStep(
×
1097
            step=self.step,
1098
            granularity=self.granularity.to_api_enum(),
1099
        )
1100

1101

1102
@dataclass
1✔
1103
class Provenance:
1✔
1104
    """Provenance information as triplet of citation, license and uri"""
1105

1106
    citation: str
1✔
1107
    license: str
1✔
1108
    uri: str
1✔
1109

1110
    @classmethod
1✔
1111
    def from_response(cls, response: geoengine_openapi_client.Provenance) -> Provenance:
1✔
1112
        """Parse an http response to a `Provenance` object"""
1113
        return Provenance(response.citation, response.license, response.uri)
1✔
1114

1115
    def to_api_dict(self) -> geoengine_openapi_client.Provenance:
1✔
1116
        return geoengine_openapi_client.Provenance(
1✔
1117
            citation=self.citation,
1118
            license=self.license,
1119
            uri=self.uri,
1120
        )
1121

1122

1123
@dataclass
1✔
1124
class ProvenanceEntry:
1✔
1125
    """Provenance of a dataset"""
1126

1127
    data: list[DataId]
1✔
1128
    provenance: Provenance
1✔
1129

1130
    @classmethod
1✔
1131
    def from_response(cls, response: geoengine_openapi_client.ProvenanceEntry) -> ProvenanceEntry:
1✔
1132
        """Parse an http response to a `ProvenanceEntry` object"""
1133

1134
        dataset = [DataId.from_response(data) for data in response.data]
1✔
1135
        provenance = Provenance.from_response(response.provenance)
1✔
1136

1137
        return ProvenanceEntry(dataset, provenance)
1✔
1138

1139

1140
class Symbology:
1✔
1141
    """Base class for symbology"""
1142

1143
    @abstractmethod
1✔
1144
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1145
        pass
×
1146

1147
    @staticmethod
1✔
1148
    def from_response(response: geoengine_openapi_client.Symbology) -> Symbology:
1✔
1149
        """Parse an http response to a `Symbology` object"""
1150
        inner = response.actual_instance
1✔
1151

1152
        if isinstance(
1✔
1153
            inner,
1154
            geoengine_openapi_client.PointSymbology
1155
            | geoengine_openapi_client.LineSymbology
1156
            | geoengine_openapi_client.PolygonSymbology,
1157
        ):
1158
            # return VectorSymbology.from_response_vector(response)
1159
            return VectorSymbology()  # TODO: implement
×
1160
        if isinstance(inner, geoengine_openapi_client.RasterSymbology):
1✔
1161
            return RasterSymbology.from_response_raster(inner)
1✔
1162

1163
        raise InputException("Invalid symbology type")
×
1164

1165
    def __repr__(self):
1✔
1166
        "Symbology"
1167

1168

1169
class VectorSymbology(Symbology):
1✔
1170
    """A vector symbology"""
1171

1172
    # TODO: implement
1173

1174
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1175
        return None  # type: ignore
×
1176

1177

1178
class RasterColorizer:
1✔
1179
    """Base class for raster colorizer"""
1180

1181
    @classmethod
1✔
1182
    def from_response(cls, response: geoengine_openapi_client.RasterColorizer) -> RasterColorizer:
1✔
1183
        """Parse an http response to a `RasterColorizer` object"""
1184
        inner = response.actual_instance
1✔
1185

1186
        if isinstance(inner, geoengine_openapi_client.SingleBandRasterColorizer):
1✔
1187
            return SingleBandRasterColorizer.from_single_band_response(inner)
1✔
1188
        if isinstance(inner, geoengine_openapi_client.MultiBandRasterColorizer):
1✔
1189
            return MultiBandRasterColorizer.from_multi_band_response(inner)
1✔
1190

1191
        raise GeoEngineException({"message": "Unknown RasterColorizer type"})
×
1192

1193
    @abstractmethod
1✔
1194
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1195
        pass
×
1196

1197

1198
@dataclass
1✔
1199
class SingleBandRasterColorizer(RasterColorizer):
1✔
1200
    """A raster colorizer for a specified band"""
1201

1202
    band: int
1✔
1203
    band_colorizer: Colorizer
1✔
1204

1205
    @staticmethod
1✔
1206
    def from_single_band_response(response: geoengine_openapi_client.SingleBandRasterColorizer) -> RasterColorizer:
1✔
1207
        return SingleBandRasterColorizer(response.band, Colorizer.from_response(response.band_colorizer))
1✔
1208

1209
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1210
        return geoengine_openapi_client.RasterColorizer(
1✔
1211
            geoengine_openapi_client.SingleBandRasterColorizer(
1212
                type="singleBand",
1213
                band=self.band,
1214
                band_colorizer=self.band_colorizer.to_api_dict(),
1215
            )
1216
        )
1217

1218

1219
@dataclass
1✔
1220
class MultiBandRasterColorizer(RasterColorizer):
1✔
1221
    """A raster colorizer for multiple bands"""
1222

1223
    blue_band: int
1✔
1224
    blue_max: float
1✔
1225
    blue_min: float
1✔
1226
    blue_scale: float | None
1✔
1227
    green_band: int
1✔
1228
    green_max: float
1✔
1229
    green_min: float
1✔
1230
    green_scale: float | None
1✔
1231
    red_band: int
1✔
1232
    red_max: float
1✔
1233
    red_min: float
1✔
1234
    red_scale: float | None
1✔
1235

1236
    @staticmethod
1✔
1237
    def from_multi_band_response(response: geoengine_openapi_client.MultiBandRasterColorizer) -> RasterColorizer:
1✔
1238
        return MultiBandRasterColorizer(
1✔
1239
            response.blue_band,
1240
            response.blue_max,
1241
            response.blue_min,
1242
            response.blue_scale,
1243
            response.green_band,
1244
            response.green_max,
1245
            response.green_min,
1246
            response.green_scale,
1247
            response.red_band,
1248
            response.red_max,
1249
            response.red_min,
1250
            response.red_scale,
1251
        )
1252

1253
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1254
        return geoengine_openapi_client.RasterColorizer(
×
1255
            geoengine_openapi_client.MultiBandRasterColorizer(
1256
                type="multiBand",
1257
                blue_band=self.blue_band,
1258
                blue_max=self.blue_max,
1259
                blue_min=self.blue_min,
1260
                blue_scale=self.blue_scale,
1261
                green_band=self.green_band,
1262
                green_max=self.green_max,
1263
                green_min=self.green_min,
1264
                green_scale=self.green_scale,
1265
                red_band=self.red_band,
1266
                red_max=self.red_max,
1267
                red_min=self.red_min,
1268
                red_scale=self.red_scale,
1269
            )
1270
        )
1271

1272

1273
class RasterSymbology(Symbology):
1✔
1274
    """A raster symbology"""
1275

1276
    opacity: float
1✔
1277
    raster_colorizer: RasterColorizer
1✔
1278

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

1282
        self.raster_colorizer = raster_colorizer
1✔
1283
        self.opacity = opacity
1✔
1284

1285
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1286
        """Convert the raster symbology to a dictionary"""
1287

1288
        return geoengine_openapi_client.Symbology(
1✔
1289
            geoengine_openapi_client.RasterSymbology(
1290
                type="raster",
1291
                raster_colorizer=self.raster_colorizer.to_api_dict(),
1292
                opacity=self.opacity,
1293
            )
1294
        )
1295

1296
    @staticmethod
1✔
1297
    def from_response_raster(response: geoengine_openapi_client.RasterSymbology) -> RasterSymbology:
1✔
1298
        """Parse an http response to a `RasterSymbology` object"""
1299

1300
        raster_colorizer = RasterColorizer.from_response(response.raster_colorizer)
1✔
1301

1302
        return RasterSymbology(raster_colorizer, response.opacity)
1✔
1303

1304
    def __repr__(self) -> str:
1✔
1305
        return str(self.__class__) + f"({self.raster_colorizer}, {self.opacity})"
×
1306

1307
    def __eq__(self, value):
1✔
1308
        """Check if two RasterSymbologies are equal"""
1309

1310
        if not isinstance(value, self.__class__):
1✔
1311
            return False
×
1312
        return self.opacity == value.opacity and self.raster_colorizer == value.raster_colorizer
1✔
1313

1314

1315
class DataId:  # pylint: disable=too-few-public-methods
1✔
1316
    """Base class for data ids"""
1317

1318
    @classmethod
1✔
1319
    def from_response(cls, response: geoengine_openapi_client.DataId) -> DataId:
1✔
1320
        """Parse an http response to a `DataId` object"""
1321
        inner = response.actual_instance
1✔
1322

1323
        if isinstance(inner, geoengine_openapi_client.InternalDataId):
1✔
1324
            return InternalDataId.from_response_internal(inner)
1✔
1325
        if isinstance(inner, geoengine_openapi_client.ExternalDataId):
×
1326
            return ExternalDataId.from_response_external(inner)
×
1327

1328
        raise GeoEngineException({"message": "Unknown DataId type"})
×
1329

1330
    @abstractmethod
1✔
1331
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1332
        pass
×
1333

1334

1335
class InternalDataId(DataId):
1✔
1336
    """An internal data id"""
1337

1338
    __dataset_id: UUID
1✔
1339

1340
    def __init__(self, dataset_id: UUID):
1✔
1341
        self.__dataset_id = dataset_id
1✔
1342

1343
    @classmethod
1✔
1344
    def from_response_internal(cls, response: geoengine_openapi_client.InternalDataId) -> InternalDataId:
1✔
1345
        """Parse an http response to a `InternalDataId` object"""
1346
        return InternalDataId(UUID(response.dataset_id))
1✔
1347

1348
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1349
        return geoengine_openapi_client.DataId(
×
1350
            geoengine_openapi_client.InternalDataId(type="internal", dataset_id=str(self.__dataset_id))
1351
        )
1352

1353
    def __str__(self) -> str:
1✔
1354
        return str(self.__dataset_id)
×
1355

1356
    def __repr__(self) -> str:
1✔
1357
        """Display representation of an internal data id"""
1358
        return str(self)
×
1359

1360
    def __eq__(self, other) -> bool:
1✔
1361
        """Check if two internal data ids are equal"""
1362
        if not isinstance(other, self.__class__):
1✔
1363
            return False
×
1364

1365
        return self.__dataset_id == other.__dataset_id  # pylint: disable=protected-access
1✔
1366

1367

1368
class ExternalDataId(DataId):
1✔
1369
    """An external data id"""
1370

1371
    __provider_id: UUID
1✔
1372
    __layer_id: str
1✔
1373

1374
    def __init__(self, provider_id: UUID, layer_id: str):
1✔
1375
        self.__provider_id = provider_id
×
1376
        self.__layer_id = layer_id
×
1377

1378
    @classmethod
1✔
1379
    def from_response_external(cls, response: geoengine_openapi_client.ExternalDataId) -> ExternalDataId:
1✔
1380
        """Parse an http response to a `ExternalDataId` object"""
1381

1382
        return ExternalDataId(UUID(response.provider_id), response.layer_id)
×
1383

1384
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1385
        return geoengine_openapi_client.DataId(
×
1386
            geoengine_openapi_client.ExternalDataId(
1387
                type="external",
1388
                provider_id=str(self.__provider_id),
1389
                layer_id=self.__layer_id,
1390
            )
1391
        )
1392

1393
    def __str__(self) -> str:
1✔
1394
        return f"{self.__provider_id}:{self.__layer_id}"
×
1395

1396
    def __repr__(self) -> str:
1✔
1397
        """Display representation of an external data id"""
1398
        return str(self)
×
1399

1400
    def __eq__(self, other) -> bool:
1✔
1401
        """Check if two external data ids are equal"""
1402
        if not isinstance(other, self.__class__):
×
1403
            return False
×
1404

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

1407

1408
class Measurement:  # pylint: disable=too-few-public-methods
1✔
1409
    """
1410
    Base class for measurements
1411
    """
1412

1413
    @staticmethod
1✔
1414
    def from_response(response: geoengine_openapi_client.Measurement) -> Measurement:
1✔
1415
        """
1416
        Parse a result descriptor from an http response
1417
        """
1418
        inner = response.actual_instance
1✔
1419

1420
        if isinstance(inner, geoengine_openapi_client.UnitlessMeasurement):
1✔
1421
            return UnitlessMeasurement()
1✔
1422
        if isinstance(inner, geoengine_openapi_client.ContinuousMeasurement):
1✔
1423
            return ContinuousMeasurement.from_response_continuous(inner)
1✔
1424
        if isinstance(inner, geoengine_openapi_client.ClassificationMeasurement):
×
1425
            return ClassificationMeasurement.from_response_classification(inner)
×
1426

1427
        raise TypeException("Unknown `Measurement` type")
×
1428

1429
    @abstractmethod
1✔
1430
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1431
        pass
×
1432

1433

1434
class UnitlessMeasurement(Measurement):
1✔
1435
    """A measurement that is unitless"""
1436

1437
    def __str__(self) -> str:
1✔
1438
        """String representation of a unitless measurement"""
1439
        return "unitless"
1✔
1440

1441
    def __repr__(self) -> str:
1✔
1442
        """Display representation of a unitless measurement"""
1443
        return str(self)
×
1444

1445
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1446
        return geoengine_openapi_client.Measurement(geoengine_openapi_client.UnitlessMeasurement(type="unitless"))
1✔
1447

1448

1449
class ContinuousMeasurement(Measurement):
1✔
1450
    """A measurement that is continuous"""
1451

1452
    __measurement: str
1✔
1453
    __unit: str | None
1✔
1454

1455
    def __init__(self, measurement: str, unit: str | None) -> None:
1✔
1456
        """Initialize a new `ContiuousMeasurement`"""
1457

1458
        super().__init__()
1✔
1459

1460
        self.__measurement = measurement
1✔
1461
        self.__unit = unit
1✔
1462

1463
    @staticmethod
1✔
1464
    def from_response_continuous(response: geoengine_openapi_client.ContinuousMeasurement) -> ContinuousMeasurement:
1✔
1465
        """Initialize a new `ContiuousMeasurement from a JSON response"""
1466

1467
        return ContinuousMeasurement(response.measurement, response.unit)
1✔
1468

1469
    def __str__(self) -> str:
1✔
1470
        """String representation of a continuous measurement"""
1471

1472
        if self.__unit is None:
1✔
1473
            return self.__measurement
1✔
1474

1475
        return f"{self.__measurement} ({self.__unit})"
×
1476

1477
    def __repr__(self) -> str:
1✔
1478
        """Display representation of a continuous measurement"""
1479
        return str(self)
×
1480

1481
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1482
        return geoengine_openapi_client.Measurement(
1✔
1483
            geoengine_openapi_client.ContinuousMeasurement(
1484
                type="continuous", measurement=self.__measurement, unit=self.__unit
1485
            )
1486
        )
1487

1488
    @property
1✔
1489
    def measurement(self) -> str:
1✔
1490
        return self.__measurement
×
1491

1492
    @property
1✔
1493
    def unit(self) -> str | None:
1✔
1494
        return self.__unit
×
1495

1496

1497
class ClassificationMeasurement(Measurement):
1✔
1498
    """A measurement that is a classification"""
1499

1500
    __measurement: str
1✔
1501
    __classes: dict[int, str]
1✔
1502

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

1506
        super().__init__()
1✔
1507

1508
        self.__measurement = measurement
1✔
1509
        self.__classes = classes
1✔
1510

1511
    @staticmethod
1✔
1512
    def from_response_classification(
1✔
1513
        response: geoengine_openapi_client.ClassificationMeasurement,
1514
    ) -> ClassificationMeasurement:
1515
        """Initialize a new `ClassificationMeasurement from a JSON response"""
1516

1517
        measurement = response.measurement
×
1518

1519
        str_classes: dict[str, str] = response.classes
×
1520
        classes = {int(k): v for k, v in str_classes.items()}
×
1521

1522
        return ClassificationMeasurement(measurement, classes)
×
1523

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

1527
        return geoengine_openapi_client.Measurement(
1✔
1528
            geoengine_openapi_client.ClassificationMeasurement(
1529
                type="classification", measurement=self.__measurement, classes=str_classes
1530
            )
1531
        )
1532

1533
    def __str__(self) -> str:
1✔
1534
        """String representation of a classification measurement"""
1535
        classes_str = ", ".join(f"{k}: {v}" for k, v in self.__classes.items())
×
1536
        return f"{self.__measurement} ({classes_str})"
×
1537

1538
    def __repr__(self) -> str:
1✔
1539
        """Display representation of a classification measurement"""
1540
        return str(self)
×
1541

1542
    @property
1✔
1543
    def measurement(self) -> str:
1✔
1544
        return self.__measurement
×
1545

1546
    @property
1✔
1547
    def classes(self) -> dict[int, str]:
1✔
1548
        return self.__classes
×
1549

1550

1551
class GeoTransform:
1✔
1552
    """The `GeoTransform` specifies the relationship between pixel coordinates and geographic coordinates."""
1553

1554
    x_min: float
1✔
1555
    y_max: float
1✔
1556
    """In Geo Engine, x_pixel_size is always positive."""
1✔
1557
    x_pixel_size: float
1✔
1558
    """In Geo Engine, y_pixel_size is always negative."""
1✔
1559
    y_pixel_size: float
1✔
1560

1561
    def __init__(self, x_min: float, y_max: float, x_pixel_size: float, y_pixel_size: float):
1✔
1562
        """Initialize a new `GeoTransform`"""
1563

1564
        assert x_pixel_size > 0, "In Geo Engine, x_pixel_size is always positive."
1✔
1565
        assert y_pixel_size < 0, "In Geo Engine, y_pixel_size is always negative."
1✔
1566

1567
        self.x_min = x_min
1✔
1568
        self.y_max = y_max
1✔
1569
        self.x_pixel_size = x_pixel_size
1✔
1570
        self.y_pixel_size = y_pixel_size
1✔
1571

1572
    @classmethod
1✔
1573
    def from_response_gdal_geo_transform(
1✔
1574
        cls, response: geoengine_openapi_client.GdalDatasetGeoTransform
1575
    ) -> GeoTransform:
1576
        """Parse a geotransform from an HTTP JSON response"""
1577
        return GeoTransform(
×
1578
            x_min=response.origin_coordinate.x,
1579
            y_max=response.origin_coordinate.y,
1580
            x_pixel_size=response.x_pixel_size,
1581
            y_pixel_size=response.y_pixel_size,
1582
        )
1583

1584
    @classmethod
1✔
1585
    def from_response(cls, response: geoengine_openapi_client.GeoTransform) -> GeoTransform:
1✔
1586
        """Parse a geotransform from an HTTP JSON response"""
1587

1588
        return GeoTransform(
1✔
1589
            x_min=response.origin_coordinate.x,
1590
            y_max=response.origin_coordinate.y,
1591
            x_pixel_size=response.x_pixel_size,
1592
            y_pixel_size=response.y_pixel_size,
1593
        )
1594

1595
    def to_api_dict(self) -> geoengine_openapi_client.GeoTransform:
1✔
1596
        return geoengine_openapi_client.GeoTransform(
1✔
1597
            origin_coordinate=geoengine_openapi_client.Coordinate2D(
1598
                x=self.x_min,
1599
                y=self.y_max,
1600
            ),
1601
            x_pixel_size=self.x_pixel_size,
1602
            y_pixel_size=self.y_pixel_size,
1603
        )
1604

1605
    def to_api_dict_gdal_geo_transform(self) -> geoengine_openapi_client.GdalDatasetGeoTransform:
1✔
1606
        return geoengine_openapi_client.GdalDatasetGeoTransform(
×
1607
            origin_coordinate=geoengine_openapi_client.Coordinate2D(
1608
                x=self.x_min,
1609
                y=self.y_max,
1610
            ),
1611
            x_pixel_size=self.x_pixel_size,
1612
            y_pixel_size=self.y_pixel_size,
1613
        )
1614

1615
    def to_gdal(self) -> tuple[float, float, float, float, float, float]:
1✔
1616
        """Convert to a GDAL geotransform"""
1617
        return (self.x_min, self.x_pixel_size, 0, self.y_max, 0, self.y_pixel_size)
×
1618

1619
    def __str__(self) -> str:
1✔
1620
        return (
×
1621
            f"Origin: ({self.x_min}, {self.y_max}), "
1622
            f"X Pixel Size: {self.x_pixel_size}, "
1623
            f"Y Pixel Size: {self.y_pixel_size}"
1624
        )
1625

1626
    def __repr__(self) -> str:
1✔
1627
        return str(self)
×
1628

1629
    @property
1✔
1630
    def x_half_pixel_size(self) -> float:
1✔
1631
        return self.x_pixel_size / 2.0
1✔
1632

1633
    @property
1✔
1634
    def y_half_pixel_size(self) -> float:
1✔
1635
        return self.y_pixel_size / 2.0
1✔
1636

1637
    def pixel_x_to_coord_x(self, pixel: int) -> float:
1✔
1638
        return self.x_min + pixel * self.x_pixel_size
1✔
1639

1640
    def pixel_y_to_coord_y(self, pixel: int) -> float:
1✔
1641
        return self.y_max + pixel * self.y_pixel_size
1✔
1642

1643
    def coord_to_pixel_ul(self, x_cord: float, y_coord: float) -> GridIdx2D:
1✔
1644
        """Convert a coordinate to a pixel index rould towards top left"""
1645
        return GridIdx2D(
×
1646
            x_idx=int(np.floor((x_cord - self.x_min) / self.x_pixel_size)),
1647
            y_idx=int(np.ceil((y_coord - self.y_max) / self.y_pixel_size)),
1648
        )
1649

1650
    def coord_to_pixel_lr(self, x_cord: float, y_coord: float) -> GridIdx2D:
1✔
1651
        """Convert a coordinate to a pixel index ound towards lower right"""
1652
        return GridIdx2D(
×
1653
            x_idx=int(np.ceil((x_cord - self.x_min) / self.x_pixel_size)),
1654
            y_idx=int(np.floor((y_coord - self.y_max) / self.y_pixel_size)),
1655
        )
1656

1657
    def pixel_ul_to_coord(self, x_pixel: int, y_pixel: int) -> tuple[float, float]:
1✔
1658
        """Convert a pixel position into a coordinate"""
1659
        x = self.pixel_x_to_coord_x(x_pixel)
×
1660
        y = self.pixel_y_to_coord_y(y_pixel)
×
1661
        return (x, y)
×
1662

1663
    def pixel_lr_to_coord(self, x_pixel: int, y_pixel: int) -> tuple[float, float]:
1✔
1664
        (x, y) = self.pixel_ul_to_coord(x_pixel, y_pixel)
×
1665
        return (x + self.x_pixel_size, y + self.y_pixel_size)
×
1666

1667
    def pixel_center_to_coord(self, x_pixel, y_pixel) -> tuple[float, float]:
1✔
1668
        (x, y) = self.pixel_ul_to_coord(x_pixel, y_pixel)
×
1669
        return (x + self.x_half_pixel_size, y + self.y_half_pixel_size)
×
1670

1671
    def spatial_resolution(self) -> SpatialResolution:
1✔
1672
        return SpatialResolution(x_resolution=abs(self.x_pixel_size), y_resolution=abs(self.y_pixel_size))
×
1673

1674
    def spatial_to_grid_bounds(self, bounds: SpatialPartition2D | BoundingBox2D) -> GridBoundingBox2D:
1✔
1675
        """Converts a BoundingBox2D or a SpatialPartition2D into a GridBoundingBox2D"""
1676
        ul = self.coord_to_pixel_ul(bounds.xmin, bounds.ymax)
×
1677
        rl = self.coord_to_pixel_lr(bounds.xmax, bounds.ymin)
×
1678
        return GridBoundingBox2D(top_left_idx=ul, bottom_right_idx=rl)
×
1679

1680
    def grid_bounds_to_spatial_bounds(self, bounds: GridBoundingBox2D) -> SpatialPartition2D:
1✔
1681
        """Converts a GridBoundingBox2D into a SpatialPartition2D"""
1682
        xmin, ymax = self.pixel_ul_to_coord(bounds.top_left_idx.x_idx, bounds.top_left_idx.y_idx)
×
1683
        xmax, ymin = self.pixel_lr_to_coord(bounds.bottom_right_idx.x_idx, bounds.bottom_right_idx.y_idx)
×
1684
        return SpatialPartition2D(xmin, ymin, xmax, ymax)
×
1685

1686
    def __eq__(self, other) -> bool:
1✔
1687
        """Check if two geotransforms are equal"""
1688
        if not isinstance(other, GeoTransform):
×
1689
            return False
×
1690

1691
        return (
×
1692
            self.x_min == other.x_min
1693
            and self.y_max == other.y_max
1694
            and self.x_pixel_size == other.x_pixel_size
1695
            and self.y_pixel_size == other.y_pixel_size
1696
        )
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

© 2026 Coveralls, Inc