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

geo-engine / geoengine-python / 16367912334

18 Jul 2025 10:06AM UTC coverage: 76.934% (+0.1%) from 76.806%
16367912334

push

github

web-flow
ci: use Ruff as new formatter and linter (#233)

* wip

* pycodestyle

* update dependencies

* skl2onnx

* use ruff

* apply formatter

* apply lint auto fixes

* manually apply lints

* change check

* ruff ci from branch

2805 of 3646 relevant lines covered (76.93%)

0.77 hits per line

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

84.83
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 numpy as np
1✔
17
from attr import dataclass
1✔
18

19
from geoengine.colorizer import Colorizer
1✔
20
from geoengine.error import GeoEngineException, InputException, TypeException
1✔
21

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

24

25
class SpatialBounds:
1✔
26
    """A spatial bounds object"""
27

28
    xmin: float
1✔
29
    ymin: float
1✔
30
    xmax: float
1✔
31
    ymax: float
1✔
32

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

38
        self.xmin = xmin
1✔
39
        self.ymin = ymin
1✔
40
        self.xmax = xmax
1✔
41
        self.ymax = ymax
1✔
42

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

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

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

58
        return (self.xmin, self.ymin, self.xmax, self.ymax)
1✔
59

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

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

68

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

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

84
    @staticmethod
1✔
85
    def from_response(response: geoengine_openapi_client.BoundingBox2D) -> BoundingBox2D:
1✔
86
        """create a `BoundingBox2D` from an API response"""
87
        lower_left = response.lower_left_coordinate
1✔
88
        upper_right = response.upper_right_coordinate
1✔
89

90
        return BoundingBox2D(
1✔
91
            lower_left.x,
92
            lower_left.y,
93
            upper_right.x,
94
            upper_right.y,
95
        )
96

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

100

101
class SpatialPartition2D(SpatialBounds):
1✔
102
    """A 2D spatial partition."""
103

104
    @staticmethod
1✔
105
    def from_response(response: geoengine_openapi_client.SpatialPartition2D) -> SpatialPartition2D:
1✔
106
        """create a `SpatialPartition2D` from an API response"""
107
        upper_left = response.upper_left_coordinate
1✔
108
        lower_right = response.lower_right_coordinate
1✔
109

110
        return SpatialPartition2D(
1✔
111
            upper_left.x,
112
            lower_right.y,
113
            lower_right.x,
114
            upper_left.y,
115
        )
116

117
    def to_api_dict(self) -> geoengine_openapi_client.SpatialPartition2D:
1✔
118
        return geoengine_openapi_client.SpatialPartition2D(
1✔
119
            upper_left_coordinate=geoengine_openapi_client.Coordinate2D(
120
                x=self.xmin,
121
                y=self.ymax,
122
            ),
123
            lower_right_coordinate=geoengine_openapi_client.Coordinate2D(
124
                x=self.xmax,
125
                y=self.ymin,
126
            ),
127
        )
128

129
    def to_bounding_box(self) -> BoundingBox2D:
1✔
130
        """convert to a `BoundingBox2D`"""
131
        return BoundingBox2D(self.xmin, self.ymin, self.xmax, self.ymax)
×
132

133

134
class TimeInterval:
1✔
135
    """'A time interval."""
136

137
    start: np.datetime64
1✔
138
    end: np.datetime64 | None
1✔
139

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

143
        if isinstance(start, np.datetime64):
1✔
144
            self.start = start
1✔
145
        elif isinstance(start, datetime):
1✔
146
            # We assume that a datetime without a timezone means UTC
147
            if start.tzinfo is not None:
1✔
148
                start = start.astimezone(tz=timezone.utc).replace(tzinfo=None)
1✔
149
            self.start = np.datetime64(start)
1✔
150
        else:
151
            raise InputException("`start` must be of type `datetime.datetime` or `numpy.datetime64`")
×
152

153
        if end is None:
1✔
154
            self.end = None
1✔
155
        elif isinstance(end, np.datetime64):
1✔
156
            self.end = end
1✔
157
        elif isinstance(end, datetime):
1✔
158
            # We assume that a datetime without a timezone means UTC
159
            if end.tzinfo is not None:
1✔
160
                end = end.astimezone(tz=timezone.utc).replace(tzinfo=None)
1✔
161
            self.end = np.datetime64(end)
1✔
162
        else:
163
            raise InputException("`end` must be of type `datetime.datetime` or `numpy.datetime64`")
×
164

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

169
    def is_instant(self) -> bool:
1✔
170
        return self.end is None or self.start == self.end
×
171

172
    @property
1✔
173
    def time_str(self) -> str:
1✔
174
        """
175
        Return the time instance or interval as a string representation
176
        """
177

178
        start_iso = TimeInterval.__datetime_to_iso_str(self.start)
1✔
179

180
        if self.end is None or self.start == self.end:
1✔
181
            return start_iso
1✔
182

183
        end_iso = TimeInterval.__datetime_to_iso_str(self.end)
1✔
184

185
        return start_iso + "/" + end_iso
1✔
186

187
    @staticmethod
1✔
188
    def from_response(response: geoengine_openapi_client.models.TimeInterval) -> TimeInterval:
1✔
189
        """create a `TimeInterval` from an API response"""
190

191
        if response.start is None:
1✔
192
            raise TypeException("TimeInterval must have a start")
×
193

194
        start = cast(int, response.start)
1✔
195
        end = None
1✔
196
        if response.end is not None:
1✔
197
            end = cast(int, response.end)
1✔
198

199
        if start == end:
1✔
200
            end = None
1✔
201

202
        return TimeInterval(
1✔
203
            np.datetime64(start, "ms"),
204
            np.datetime64(end, "ms") if end is not None else None,
205
        )
206

207
    def __repr__(self) -> str:
1✔
208
        return f"TimeInterval(start={self.start}, end={self.end})"
×
209

210
    def to_api_dict(self) -> geoengine_openapi_client.TimeInterval:
1✔
211
        """create a openapi `TimeInterval` from self"""
212
        start = self.start.astype("datetime64[ms]").astype(int)
×
213
        end = self.end.astype("datetime64[ms]").astype(int) if self.end is not None else None
×
214

215
        # The openapi Timeinterval does not accept end: None. So we set it to start IF self is an instant.
216
        end = end if end is not None else start
×
217

218
        print(self, start, end)
×
219

220
        return geoengine_openapi_client.TimeInterval(start=int(start), end=int(end))
×
221

222
    @staticmethod
1✔
223
    def __datetime_to_iso_str(timestamp: np.datetime64) -> str:
1✔
224
        return str(np.datetime_as_string(timestamp, unit="ms", timezone="UTC")).replace("Z", "+00:00")
1✔
225

226
    def __eq__(self, other: Any) -> bool:
1✔
227
        """Check if two `TimeInterval` objects are equal."""
228
        if not isinstance(other, TimeInterval):
×
229
            return False
×
230
        return self.start == other.start and self.end == other.end
×
231

232

233
class SpatialResolution:
1✔
234
    """'A spatial resolution."""
235

236
    x_resolution: float
1✔
237
    y_resolution: float
1✔
238

239
    def __init__(self, x_resolution: float, y_resolution: float) -> None:
1✔
240
        """Initialize a new `SpatialResolution` object"""
241
        if x_resolution <= 0 or y_resolution <= 0:
1✔
242
            raise InputException("Resolution: Must be positive")
×
243

244
        self.x_resolution = x_resolution
1✔
245
        self.y_resolution = y_resolution
1✔
246

247
    def to_api_dict(self) -> geoengine_openapi_client.SpatialResolution:
1✔
248
        return geoengine_openapi_client.SpatialResolution(
1✔
249
            x=self.x_resolution,
250
            y=self.y_resolution,
251
        )
252

253
    @staticmethod
1✔
254
    def from_response(response: geoengine_openapi_client.SpatialResolution) -> SpatialResolution:
1✔
255
        """create a `SpatialResolution` from an API response"""
256
        return SpatialResolution(x_resolution=response.x, y_resolution=response.y)
1✔
257

258
    def as_tuple(self) -> tuple[float, float]:
1✔
259
        return (self.x_resolution, self.y_resolution)
×
260

261
    def __str__(self) -> str:
1✔
262
        return str(f"{self.x_resolution},{self.y_resolution}")
1✔
263

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

267

268
class QueryRectangle:
1✔
269
    """
270
    A multi-dimensional query rectangle, consisting of spatial and temporal information.
271
    """
272

273
    __spatial_bounds: BoundingBox2D
1✔
274
    __time_interval: TimeInterval
1✔
275
    __resolution: SpatialResolution
1✔
276
    __srs: str
1✔
277

278
    def __init__(
1✔
279
        self,
280
        spatial_bounds: BoundingBox2D | tuple[float, float, float, float],
281
        time_interval: TimeInterval | tuple[datetime, datetime | None],
282
        resolution: SpatialResolution | tuple[float, float],
283
        srs="EPSG:4326",
284
    ) -> None:
285
        """
286
        Initialize a new `QueryRectangle` object
287

288
        Parameters
289
        ----------
290
        spatial_bounds
291
            The spatial bounds of the query rectangle.
292
            Either a `BoundingBox2D` or a tuple of floats (xmin, ymin, xmax, ymax)
293
        time_interval
294
            The time interval of the query rectangle.
295
            Either a `TimeInterval` or a tuple of `datetime.datetime` objects (start, end)
296
        resolution
297
            The spatial resolution of the query rectangle.
298
            Either a `SpatialResolution` or a tuple of floats (x_resolution, y_resolution)
299
        """
300

301
        if not isinstance(spatial_bounds, BoundingBox2D):
1✔
302
            spatial_bounds = BoundingBox2D(*spatial_bounds)
×
303
        if not isinstance(time_interval, TimeInterval):
1✔
304
            time_interval = TimeInterval(*time_interval)
×
305
        if not isinstance(resolution, SpatialResolution):
1✔
306
            resolution = SpatialResolution(*resolution)
×
307

308
        self.__spatial_bounds = spatial_bounds
1✔
309
        self.__time_interval = time_interval
1✔
310
        self.__resolution = resolution
1✔
311
        self.__srs = srs
1✔
312

313
    @property
1✔
314
    def bbox_str(self) -> str:
1✔
315
        """
316
        A comma-separated string representation of the spatial bounds
317
        """
318
        return self.__spatial_bounds.as_bbox_str()
1✔
319

320
    @property
1✔
321
    def bbox_ogc_str(self) -> str:
1✔
322
        """
323
        A comma-separated string representation of the spatial bounds with OGC axis ordering
324
        """
325
        y_axis_first = self.__srs == "EPSG:4326"
1✔
326
        return self.__spatial_bounds.as_bbox_str(y_axis_first=y_axis_first)
1✔
327

328
    @property
1✔
329
    def bbox_ogc(self) -> tuple[float, float, float, float]:
1✔
330
        """
331
        Return the bbox with OGC axis ordering of the srs
332
        """
333

334
        # TODO: properly handle axis order
335
        y_axis_first = self.__srs == "EPSG:4326"
1✔
336
        return self.__spatial_bounds.as_bbox_tuple(y_axis_first=y_axis_first)
1✔
337

338
    @property
1✔
339
    def resolution_ogc(self) -> tuple[float, float]:
1✔
340
        """
341
        Return the resolution in OGC style
342
        """
343
        # TODO: properly handle axis order
344
        res = self.__resolution
1✔
345

346
        # TODO: why is the y resolution in this case negative but not in all other cases?
347
        if self.__srs == "EPSG:4326":
1✔
348
            return (-res.y_resolution, res.x_resolution)
1✔
349

350
        return res.as_tuple()
×
351

352
    @property
1✔
353
    def time(self) -> TimeInterval:
1✔
354
        """
355
        Return the time instance or interval
356
        """
357
        return self.__time_interval
1✔
358

359
    @property
1✔
360
    def spatial_bounds(self) -> BoundingBox2D:
1✔
361
        """
362
        Return the spatial bounds
363
        """
364
        return self.__spatial_bounds
1✔
365

366
    @property
1✔
367
    def spatial_resolution(self) -> SpatialResolution:
1✔
368
        """
369
        Return the spatial resolution
370
        """
371
        return self.__resolution
1✔
372

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

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

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

397
    def as_raster_query_rectangle_api_dict(self) -> geoengine_openapi_client.RasterQueryRectangle:
1✔
398
        """Return the query rectangle as a dictionary for the API"""
399
        return geoengine_openapi_client.RasterQueryRectangle(
×
400
            spatial_bounds=SpatialPartition2D(
401
                self.spatial_bounds.xmin,
402
                self.spatial_bounds.ymin,
403
                self.spatial_bounds.xmax,
404
                self.spatial_bounds.ymax,
405
            ).to_api_dict(),
406
            spatial_resolution=self.spatial_resolution.to_api_dict(),
407
            time_interval=self.time.to_api_dict(),
408
        )
409

410

411
class ResultDescriptor:  # pylint: disable=too-few-public-methods
1✔
412
    """
413
    Base class for result descriptors
414
    """
415

416
    __spatial_reference: str
1✔
417
    __time_bounds: TimeInterval | None
1✔
418
    __spatial_resolution: SpatialResolution | None
1✔
419

420
    def __init__(
1✔
421
        self,
422
        spatial_reference: str,
423
        time_bounds: TimeInterval | None = None,
424
        spatial_resolution: SpatialResolution | None = None,
425
    ) -> None:
426
        """Initialize a new `ResultDescriptor` object"""
427

428
        self.__spatial_reference = spatial_reference
1✔
429
        self.__time_bounds = time_bounds
1✔
430

431
        if spatial_resolution is None or isinstance(spatial_resolution, SpatialResolution):
1✔
432
            self.__spatial_resolution = spatial_resolution
1✔
433
        else:
434
            raise TypeException("Spatial resolution must be of type `SpatialResolution` or `None`")
×
435

436
    @staticmethod
1✔
437
    def from_response(response: geoengine_openapi_client.TypedResultDescriptor) -> ResultDescriptor:
1✔
438
        """
439
        Parse a result descriptor from an http response
440
        """
441

442
        inner = response.actual_instance
1✔
443

444
        if isinstance(inner, geoengine_openapi_client.TypedRasterResultDescriptor):
1✔
445
            return RasterResultDescriptor.from_response_raster(inner)
1✔
446
        if isinstance(inner, geoengine_openapi_client.TypedVectorResultDescriptor):
1✔
447
            return VectorResultDescriptor.from_response_vector(inner)
1✔
448
        if isinstance(inner, geoengine_openapi_client.TypedPlotResultDescriptor):
1✔
449
            return PlotResultDescriptor.from_response_plot(inner)
1✔
450

451
        raise TypeException("Unknown `ResultDescriptor` type")
×
452

453
    @classmethod
1✔
454
    def is_raster_result(cls) -> bool:
1✔
455
        """
456
        Return true if the result is of type raster
457
        """
458
        return False
×
459

460
    @classmethod
1✔
461
    def is_vector_result(cls) -> bool:
1✔
462
        """
463
        Return true if the result is of type vector
464
        """
465
        return False
1✔
466

467
    @classmethod
1✔
468
    def is_plot_result(cls) -> bool:
1✔
469
        """
470
        Return true if the result is of type plot
471
        """
472

473
        return False
×
474

475
    @property
1✔
476
    def spatial_reference(self) -> str:
1✔
477
        """Return the spatial reference"""
478

479
        return self.__spatial_reference
1✔
480

481
    @property
1✔
482
    def time_bounds(self) -> TimeInterval | None:
1✔
483
        """Return the time bounds"""
484

485
        return self.__time_bounds
1✔
486

487
    @property
1✔
488
    def spatial_resolution(self) -> SpatialResolution | None:
1✔
489
        """Return the spatial resolution"""
490

491
        return self.__spatial_resolution
1✔
492

493
    @abstractmethod
1✔
494
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
495
        pass
×
496

497
    def __iter__(self):
1✔
498
        return iter(self.to_api_dict().items())
×
499

500

501
class VectorResultDescriptor(ResultDescriptor):
1✔
502
    """
503
    A vector result descriptor
504
    """
505

506
    __spatial_bounds: BoundingBox2D | None
1✔
507
    __data_type: VectorDataType
1✔
508
    __columns: dict[str, VectorColumnInfo]
1✔
509

510
    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
1✔
511
        self,
512
        spatial_reference: str,
513
        data_type: VectorDataType,
514
        columns: dict[str, VectorColumnInfo],
515
        time_bounds: TimeInterval | None = None,
516
        spatial_bounds: BoundingBox2D | None = None,
517
    ) -> None:
518
        """Initialize a vector result descriptor"""
519
        super().__init__(spatial_reference, time_bounds, None)
1✔
520
        self.__data_type = data_type
1✔
521
        self.__columns = columns
1✔
522
        self.__spatial_bounds = spatial_bounds
1✔
523

524
    @staticmethod
1✔
525
    def from_response_vector(response: geoengine_openapi_client.TypedVectorResultDescriptor) -> VectorResultDescriptor:
1✔
526
        """Parse a vector result descriptor from an http response"""
527
        sref = response.spatial_reference
1✔
528
        data_type = VectorDataType.from_string(response.data_type)
1✔
529
        columns = {name: VectorColumnInfo.from_response(info) for name, info in response.columns.items()}
1✔
530

531
        time_bounds = None
1✔
532
        if response.time is not None:
1✔
533
            time_bounds = TimeInterval.from_response(response.time)
×
534
        spatial_bounds = None
1✔
535
        if response.bbox is not None:
1✔
536
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
×
537

538
        return VectorResultDescriptor(sref, data_type, columns, time_bounds, spatial_bounds)
1✔
539

540
    @classmethod
1✔
541
    def is_vector_result(cls) -> bool:
1✔
542
        return True
1✔
543

544
    @property
1✔
545
    def data_type(self) -> VectorDataType:
1✔
546
        """Return the data type"""
547
        return self.__data_type
1✔
548

549
    @property
1✔
550
    def spatial_reference(self) -> str:
1✔
551
        """Return the spatial reference"""
552
        return super().spatial_reference
1✔
553

554
    @property
1✔
555
    def columns(self) -> dict[str, VectorColumnInfo]:
1✔
556
        """Return the columns"""
557

558
        return self.__columns
1✔
559

560
    @property
1✔
561
    def spatial_bounds(self) -> BoundingBox2D | None:
1✔
562
        """Return the spatial bounds"""
563
        return self.__spatial_bounds
1✔
564

565
    def __repr__(self) -> str:
1✔
566
        """Display representation of the vector result descriptor"""
567
        r = ""
1✔
568
        r += f"Data type:         {self.data_type.value}\n"
1✔
569
        r += f"Spatial Reference: {self.spatial_reference}\n"
1✔
570

571
        r += "Columns:\n"
1✔
572
        for column_name in self.columns:
1✔
573
            column_info = self.columns[column_name]
1✔
574
            r += f"  {column_name}:\n"
1✔
575
            r += f"    Column Type: {column_info.data_type.value}\n"
1✔
576
            r += f"    Measurement: {column_info.measurement}\n"
1✔
577

578
        return r
1✔
579

580
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
581
        """Convert the vector result descriptor to a dictionary"""
582

583
        return geoengine_openapi_client.TypedResultDescriptor(
1✔
584
            geoengine_openapi_client.TypedVectorResultDescriptor(
585
                type="vector",
586
                data_type=self.data_type.to_api_enum(),
587
                spatial_reference=self.spatial_reference,
588
                columns={name: column_info.to_api_dict() for name, column_info in self.columns.items()},
589
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
590
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
591
                resolution=self.spatial_resolution.to_api_dict() if self.spatial_resolution is not None else None,
592
            )
593
        )
594

595

596
class FeatureDataType(str, Enum):
1✔
597
    """Vector column data type"""
598

599
    CATEGORY = "category"
1✔
600
    INT = "int"
1✔
601
    FLOAT = "float"
1✔
602
    TEXT = "text"
1✔
603
    BOOL = "bool"
1✔
604
    DATETIME = "dateTime"
1✔
605

606
    @staticmethod
1✔
607
    def from_string(data_type: str) -> FeatureDataType:
1✔
608
        """Create a new `VectorColumnDataType` from a string"""
609

610
        return FeatureDataType(data_type)
1✔
611

612
    def to_api_enum(self) -> geoengine_openapi_client.FeatureDataType:
1✔
613
        """Convert to an API enum"""
614

615
        return geoengine_openapi_client.FeatureDataType(self.value)
1✔
616

617

618
@dataclass
1✔
619
class VectorColumnInfo:
1✔
620
    """Vector column information"""
621

622
    data_type: FeatureDataType
1✔
623
    measurement: Measurement
1✔
624

625
    @staticmethod
1✔
626
    def from_response(response: geoengine_openapi_client.VectorColumnInfo) -> VectorColumnInfo:
1✔
627
        """Create a new `VectorColumnInfo` from a JSON response"""
628

629
        return VectorColumnInfo(
1✔
630
            FeatureDataType.from_string(response.data_type), Measurement.from_response(response.measurement)
631
        )
632

633
    def to_api_dict(self) -> geoengine_openapi_client.VectorColumnInfo:
1✔
634
        """Convert to a dictionary"""
635

636
        return geoengine_openapi_client.VectorColumnInfo(
1✔
637
            data_type=self.data_type.to_api_enum(),
638
            measurement=self.measurement.to_api_dict(),
639
        )
640

641

642
@dataclass(repr=False)
1✔
643
class RasterBandDescriptor:
1✔
644
    """A raster band descriptor"""
645

646
    name: str
1✔
647
    measurement: Measurement
1✔
648

649
    @classmethod
1✔
650
    def from_response(cls, response: geoengine_openapi_client.RasterBandDescriptor) -> RasterBandDescriptor:
1✔
651
        """Parse an http response to a `Provenance` object"""
652
        return RasterBandDescriptor(response.name, Measurement.from_response(response.measurement))
1✔
653

654
    def to_api_dict(self) -> geoengine_openapi_client.RasterBandDescriptor:
1✔
655
        return geoengine_openapi_client.RasterBandDescriptor(
1✔
656
            name=self.name,
657
            measurement=self.measurement.to_api_dict(),
658
        )
659

660
    def __repr__(self) -> str:
1✔
661
        """Display representation of a raster band descriptor"""
662
        return f"{self.name}: {self.measurement}"
1✔
663

664

665
def literal_raster_data_type(
1✔
666
    data_type: geoengine_openapi_client.RasterDataType,
667
) -> Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]:
668
    """Convert a `RasterDataType` to a literal"""
669

670
    data_type_map: dict[
1✔
671
        geoengine_openapi_client.RasterDataType,
672
        Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
673
    ] = {
674
        geoengine_openapi_client.RasterDataType.U8: "U8",
675
        geoengine_openapi_client.RasterDataType.U16: "U16",
676
        geoengine_openapi_client.RasterDataType.U32: "U32",
677
        geoengine_openapi_client.RasterDataType.U64: "U64",
678
        geoengine_openapi_client.RasterDataType.I8: "I8",
679
        geoengine_openapi_client.RasterDataType.I16: "I16",
680
        geoengine_openapi_client.RasterDataType.I32: "I32",
681
        geoengine_openapi_client.RasterDataType.I64: "I64",
682
        geoengine_openapi_client.RasterDataType.F32: "F32",
683
        geoengine_openapi_client.RasterDataType.F64: "F64",
684
    }
685
    return data_type_map[data_type]
1✔
686

687

688
class RasterResultDescriptor(ResultDescriptor):
1✔
689
    """
690
    A raster result descriptor
691
    """
692

693
    __data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
694
    __bands: list[RasterBandDescriptor]
1✔
695
    __spatial_bounds: SpatialPartition2D | None
1✔
696

697
    def __init__(  # pylint: disable=too-many-arguments,too-many-positional-arguments
1✔
698
        self,
699
        data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
700
        bands: list[RasterBandDescriptor],
701
        spatial_reference: str,
702
        time_bounds: TimeInterval | None = None,
703
        spatial_bounds: SpatialPartition2D | None = None,
704
        spatial_resolution: SpatialResolution | None = None,
705
    ) -> None:
706
        """Initialize a new `RasterResultDescriptor`"""
707
        super().__init__(spatial_reference, time_bounds, spatial_resolution)
1✔
708
        self.__data_type = data_type
1✔
709
        self.__bands = bands
1✔
710
        self.__spatial_bounds = spatial_bounds
1✔
711

712
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
713
        """Convert the raster result descriptor to a dictionary"""
714

715
        return geoengine_openapi_client.TypedResultDescriptor(
1✔
716
            geoengine_openapi_client.TypedRasterResultDescriptor(
717
                type="raster",
718
                data_type=self.data_type,
719
                bands=[band.to_api_dict() for band in self.__bands],
720
                spatial_reference=self.spatial_reference,
721
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
722
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
723
                resolution=self.spatial_resolution.to_api_dict() if self.spatial_resolution is not None else None,
724
            )
725
        )
726

727
    @staticmethod
1✔
728
    def from_response_raster(response: geoengine_openapi_client.TypedRasterResultDescriptor) -> RasterResultDescriptor:
1✔
729
        """Parse a raster result descriptor from an http response"""
730
        spatial_ref = response.spatial_reference
1✔
731
        data_type = literal_raster_data_type(response.data_type)
1✔
732
        bands = [RasterBandDescriptor.from_response(band) for band in response.bands]
1✔
733

734
        time_bounds = None
1✔
735
        if response.time is not None:
1✔
736
            time_bounds = TimeInterval.from_response(response.time)
×
737
        spatial_bounds = None
1✔
738
        if response.bbox is not None:
1✔
739
            spatial_bounds = SpatialPartition2D.from_response(response.bbox)
1✔
740
        spatial_resolution = None
1✔
741
        if response.resolution is not None:
1✔
742
            spatial_resolution = SpatialResolution.from_response(response.resolution)
1✔
743

744
        return RasterResultDescriptor(
1✔
745
            data_type=data_type,
746
            bands=bands,
747
            spatial_reference=spatial_ref,
748
            time_bounds=time_bounds,
749
            spatial_bounds=spatial_bounds,
750
            spatial_resolution=spatial_resolution,
751
        )
752

753
    @classmethod
1✔
754
    def is_raster_result(cls) -> bool:
1✔
755
        return True
1✔
756

757
    @property
1✔
758
    def data_type(self) -> Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]:
1✔
759
        return self.__data_type
1✔
760

761
    @property
1✔
762
    def bands(self) -> list[RasterBandDescriptor]:
1✔
763
        return self.__bands
×
764

765
    @property
1✔
766
    def spatial_bounds(self) -> SpatialPartition2D | None:
1✔
767
        return self.__spatial_bounds
1✔
768

769
    @property
1✔
770
    def spatial_reference(self) -> str:
1✔
771
        """Return the spatial reference"""
772

773
        return super().spatial_reference
1✔
774

775
    def __repr__(self) -> str:
1✔
776
        """Display representation of the raster result descriptor"""
777
        r = ""
1✔
778
        r += f"Data type:         {self.data_type}\n"
1✔
779
        r += f"Spatial Reference: {self.spatial_reference}\n"
1✔
780
        r += "Bands:\n"
1✔
781

782
        for band in self.__bands:
1✔
783
            r += f"    {band}\n"
1✔
784

785
        return r
1✔
786

787

788
class PlotResultDescriptor(ResultDescriptor):
1✔
789
    """
790
    A plot result descriptor
791
    """
792

793
    __spatial_bounds: BoundingBox2D | None
1✔
794

795
    def __init__(  # pylint: disable=too-many-arguments]
1✔
796
        self,
797
        spatial_reference: str,
798
        time_bounds: TimeInterval | None = None,
799
        spatial_bounds: BoundingBox2D | None = None,
800
    ) -> None:
801
        """Initialize a new `PlotResultDescriptor`"""
802
        super().__init__(spatial_reference, time_bounds, None)
1✔
803
        self.__spatial_bounds = spatial_bounds
1✔
804

805
    def __repr__(self) -> str:
1✔
806
        """Display representation of the plot result descriptor"""
807
        r = "Plot Result"
1✔
808

809
        return r
1✔
810

811
    @staticmethod
1✔
812
    def from_response_plot(response: geoengine_openapi_client.TypedPlotResultDescriptor) -> PlotResultDescriptor:
1✔
813
        """Create a new `PlotResultDescriptor` from a JSON response"""
814
        spatial_ref = response.spatial_reference
1✔
815

816
        time_bounds = None
1✔
817
        if response.time is not None:
1✔
818
            time_bounds = TimeInterval.from_response(response.time)
1✔
819
        spatial_bounds = None
1✔
820
        if response.bbox is not None:
1✔
821
            spatial_bounds = BoundingBox2D.from_response(response.bbox)
1✔
822

823
        return PlotResultDescriptor(
1✔
824
            spatial_reference=spatial_ref, time_bounds=time_bounds, spatial_bounds=spatial_bounds
825
        )
826

827
    @classmethod
1✔
828
    def is_plot_result(cls) -> bool:
1✔
829
        return True
1✔
830

831
    @property
1✔
832
    def spatial_reference(self) -> str:
1✔
833
        """Return the spatial reference"""
834
        return super().spatial_reference
×
835

836
    @property
1✔
837
    def spatial_bounds(self) -> BoundingBox2D | None:
1✔
838
        return self.__spatial_bounds
×
839

840
    def to_api_dict(self) -> geoengine_openapi_client.TypedResultDescriptor:
1✔
841
        """Convert the plot result descriptor to a dictionary"""
842

843
        return geoengine_openapi_client.TypedResultDescriptor(
×
844
            geoengine_openapi_client.TypedPlotResultDescriptor(
845
                type="plot",
846
                spatial_reference=self.spatial_reference,
847
                data_type="Plot",
848
                time=self.time_bounds.to_api_dict() if self.time_bounds is not None else None,
849
                bbox=self.spatial_bounds.to_api_dict() if self.spatial_bounds is not None else None,
850
            )
851
        )
852

853

854
class VectorDataType(str, Enum):
1✔
855
    """An enum of vector data types"""
856

857
    DATA = "Data"
1✔
858
    MULTI_POINT = "MultiPoint"
1✔
859
    MULTI_LINE_STRING = "MultiLineString"
1✔
860
    MULTI_POLYGON = "MultiPolygon"
1✔
861

862
    @classmethod
1✔
863
    def from_geopandas_type_name(cls, name: str) -> VectorDataType:
1✔
864
        """Resolve vector data type from geopandas geometry type"""
865

866
        name_map = {
1✔
867
            "Point": VectorDataType.MULTI_POINT,
868
            "MultiPoint": VectorDataType.MULTI_POINT,
869
            "Line": VectorDataType.MULTI_LINE_STRING,
870
            "MultiLine": VectorDataType.MULTI_LINE_STRING,
871
            "Polygon": VectorDataType.MULTI_POLYGON,
872
            "MultiPolygon": VectorDataType.MULTI_POLYGON,
873
        }
874

875
        if name in name_map:
1✔
876
            return name_map[name]
1✔
877

878
        raise InputException("Invalid vector data type")
×
879

880
    def to_api_enum(self) -> geoengine_openapi_client.VectorDataType:
1✔
881
        return geoengine_openapi_client.VectorDataType(self.value)
1✔
882

883
    @staticmethod
1✔
884
    def from_literal(literal: Literal["Data", "MultiPoint", "MultiLineString", "MultiPolygon"]) -> VectorDataType:
1✔
885
        """Resolve vector data type from literal"""
886
        return VectorDataType(literal)
×
887

888
    @staticmethod
1✔
889
    def from_api_enum(data_type: geoengine_openapi_client.VectorDataType) -> VectorDataType:
1✔
890
        """Resolve vector data type from API enum"""
891
        return VectorDataType(data_type.value)
×
892

893
    @staticmethod
1✔
894
    def from_string(string: str) -> VectorDataType:
1✔
895
        """Resolve vector data type from string"""
896
        if string not in VectorDataType.__members__.values():
1✔
897
            raise InputException("Invalid vector data type: " + string)
×
898
        return VectorDataType(string)
1✔
899

900

901
class TimeStepGranularity(Enum):
1✔
902
    """An enum of time step granularities"""
903

904
    MILLIS = "millis"
1✔
905
    SECONDS = "seconds"
1✔
906
    MINUTES = "minutes"
1✔
907
    HOURS = "hours"
1✔
908
    DAYS = "days"
1✔
909
    MONTHS = "months"
1✔
910
    YEARS = "years"
1✔
911

912
    def to_api_enum(self) -> geoengine_openapi_client.TimeGranularity:
1✔
913
        return geoengine_openapi_client.TimeGranularity(self.value)
1✔
914

915

916
@dataclass
1✔
917
class TimeStep:
1✔
918
    """A time step that consists of a granularity and a step size"""
919

920
    step: int
1✔
921
    granularity: TimeStepGranularity
1✔
922

923
    def to_api_dict(self) -> geoengine_openapi_client.TimeStep:
1✔
924
        return geoengine_openapi_client.TimeStep(
×
925
            step=self.step,
926
            granularity=self.granularity.to_api_enum(),
927
        )
928

929

930
@dataclass
1✔
931
class Provenance:
1✔
932
    """Provenance information as triplet of citation, license and uri"""
933

934
    citation: str
1✔
935
    license: str
1✔
936
    uri: str
1✔
937

938
    @classmethod
1✔
939
    def from_response(cls, response: geoengine_openapi_client.Provenance) -> Provenance:
1✔
940
        """Parse an http response to a `Provenance` object"""
941
        return Provenance(response.citation, response.license, response.uri)
1✔
942

943
    def to_api_dict(self) -> geoengine_openapi_client.Provenance:
1✔
944
        return geoengine_openapi_client.Provenance(
1✔
945
            citation=self.citation,
946
            license=self.license,
947
            uri=self.uri,
948
        )
949

950

951
@dataclass
1✔
952
class ProvenanceEntry:
1✔
953
    """Provenance of a dataset"""
954

955
    data: list[DataId]
1✔
956
    provenance: Provenance
1✔
957

958
    @classmethod
1✔
959
    def from_response(cls, response: geoengine_openapi_client.ProvenanceEntry) -> ProvenanceEntry:
1✔
960
        """Parse an http response to a `ProvenanceEntry` object"""
961

962
        dataset = [DataId.from_response(data) for data in response.data]
1✔
963
        provenance = Provenance.from_response(response.provenance)
1✔
964

965
        return ProvenanceEntry(dataset, provenance)
1✔
966

967

968
class Symbology:
1✔
969
    """Base class for symbology"""
970

971
    @abstractmethod
1✔
972
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
973
        pass
×
974

975
    @staticmethod
1✔
976
    def from_response(response: geoengine_openapi_client.Symbology) -> Symbology:
1✔
977
        """Parse an http response to a `Symbology` object"""
978
        inner = response.actual_instance
1✔
979

980
        if isinstance(
1✔
981
            inner,
982
            geoengine_openapi_client.PointSymbology
983
            | geoengine_openapi_client.LineSymbology
984
            | geoengine_openapi_client.PolygonSymbology,
985
        ):
986
            # return VectorSymbology.from_response_vector(response)
987
            return VectorSymbology()  # TODO: implement
×
988
        if isinstance(inner, geoengine_openapi_client.RasterSymbology):
1✔
989
            return RasterSymbology.from_response_raster(inner)
1✔
990

991
        raise InputException("Invalid symbology type")
×
992

993
    def __repr__(self):
1✔
994
        "Symbology"
995

996

997
class VectorSymbology(Symbology):
1✔
998
    """A vector symbology"""
999

1000
    # TODO: implement
1001

1002
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1003
        return None  # type: ignore
×
1004

1005

1006
class RasterColorizer:
1✔
1007
    """Base class for raster colorizer"""
1008

1009
    @classmethod
1✔
1010
    def from_response(cls, response: geoengine_openapi_client.RasterColorizer) -> RasterColorizer:
1✔
1011
        """Parse an http response to a `RasterColorizer` object"""
1012
        inner = response.actual_instance
1✔
1013

1014
        if isinstance(inner, geoengine_openapi_client.SingleBandRasterColorizer):
1✔
1015
            return SingleBandRasterColorizer.from_single_band_response(inner)
1✔
1016
        if isinstance(inner, geoengine_openapi_client.MultiBandRasterColorizer):
1✔
1017
            return MultiBandRasterColorizer.from_multi_band_response(inner)
1✔
1018

1019
        raise GeoEngineException({"message": "Unknown RasterColorizer type"})
×
1020

1021
    @abstractmethod
1✔
1022
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1023
        pass
×
1024

1025

1026
@dataclass
1✔
1027
class SingleBandRasterColorizer(RasterColorizer):
1✔
1028
    """A raster colorizer for a specified band"""
1029

1030
    band: int
1✔
1031
    band_colorizer: Colorizer
1✔
1032

1033
    @staticmethod
1✔
1034
    def from_single_band_response(response: geoengine_openapi_client.SingleBandRasterColorizer) -> RasterColorizer:
1✔
1035
        return SingleBandRasterColorizer(response.band, Colorizer.from_response(response.band_colorizer))
1✔
1036

1037
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1038
        return geoengine_openapi_client.RasterColorizer(
1✔
1039
            geoengine_openapi_client.SingleBandRasterColorizer(
1040
                type="singleBand",
1041
                band=self.band,
1042
                band_colorizer=self.band_colorizer.to_api_dict(),
1043
            )
1044
        )
1045

1046

1047
@dataclass
1✔
1048
class MultiBandRasterColorizer(RasterColorizer):
1✔
1049
    """A raster colorizer for multiple bands"""
1050

1051
    blue_band: int
1✔
1052
    blue_max: float
1✔
1053
    blue_min: float
1✔
1054
    blue_scale: float | None
1✔
1055
    green_band: int
1✔
1056
    green_max: float
1✔
1057
    green_min: float
1✔
1058
    green_scale: float | None
1✔
1059
    red_band: int
1✔
1060
    red_max: float
1✔
1061
    red_min: float
1✔
1062
    red_scale: float | None
1✔
1063

1064
    @staticmethod
1✔
1065
    def from_multi_band_response(response: geoengine_openapi_client.MultiBandRasterColorizer) -> RasterColorizer:
1✔
1066
        return MultiBandRasterColorizer(
1✔
1067
            response.blue_band,
1068
            response.blue_max,
1069
            response.blue_min,
1070
            response.blue_scale,
1071
            response.green_band,
1072
            response.green_max,
1073
            response.green_min,
1074
            response.green_scale,
1075
            response.red_band,
1076
            response.red_max,
1077
            response.red_min,
1078
            response.red_scale,
1079
        )
1080

1081
    def to_api_dict(self) -> geoengine_openapi_client.RasterColorizer:
1✔
1082
        return geoengine_openapi_client.RasterColorizer(
×
1083
            geoengine_openapi_client.MultiBandRasterColorizer(
1084
                type="multiBand",
1085
                blue_band=self.blue_band,
1086
                blue_max=self.blue_max,
1087
                blue_min=self.blue_min,
1088
                blue_scale=self.blue_scale,
1089
                green_band=self.green_band,
1090
                green_max=self.green_max,
1091
                green_min=self.green_min,
1092
                green_scale=self.green_scale,
1093
                red_band=self.red_band,
1094
                red_max=self.red_max,
1095
                red_min=self.red_min,
1096
                red_scale=self.red_scale,
1097
            )
1098
        )
1099

1100

1101
class RasterSymbology(Symbology):
1✔
1102
    """A raster symbology"""
1103

1104
    opacity: float
1✔
1105
    raster_colorizer: RasterColorizer
1✔
1106

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

1110
        self.raster_colorizer = raster_colorizer
1✔
1111
        self.opacity = opacity
1✔
1112

1113
    def to_api_dict(self) -> geoengine_openapi_client.Symbology:
1✔
1114
        """Convert the raster symbology to a dictionary"""
1115

1116
        return geoengine_openapi_client.Symbology(
1✔
1117
            geoengine_openapi_client.RasterSymbology(
1118
                type="raster",
1119
                raster_colorizer=self.raster_colorizer.to_api_dict(),
1120
                opacity=self.opacity,
1121
            )
1122
        )
1123

1124
    @staticmethod
1✔
1125
    def from_response_raster(response: geoengine_openapi_client.RasterSymbology) -> RasterSymbology:
1✔
1126
        """Parse an http response to a `RasterSymbology` object"""
1127

1128
        raster_colorizer = RasterColorizer.from_response(response.raster_colorizer)
1✔
1129

1130
        return RasterSymbology(raster_colorizer, response.opacity)
1✔
1131

1132
    def __repr__(self) -> str:
1✔
1133
        return str(self.__class__) + f"({self.raster_colorizer}, {self.opacity})"
×
1134

1135
    def __eq__(self, value):
1✔
1136
        """Check if two RasterSymbologies are equal"""
1137

1138
        if not isinstance(value, self.__class__):
1✔
1139
            return False
×
1140
        return self.opacity == value.opacity and self.raster_colorizer == value.raster_colorizer
1✔
1141

1142

1143
class DataId:  # pylint: disable=too-few-public-methods
1✔
1144
    """Base class for data ids"""
1145

1146
    @classmethod
1✔
1147
    def from_response(cls, response: geoengine_openapi_client.DataId) -> DataId:
1✔
1148
        """Parse an http response to a `DataId` object"""
1149
        inner = response.actual_instance
1✔
1150

1151
        if isinstance(inner, geoengine_openapi_client.InternalDataId):
1✔
1152
            return InternalDataId.from_response_internal(inner)
1✔
1153
        if isinstance(inner, geoengine_openapi_client.ExternalDataId):
×
1154
            return ExternalDataId.from_response_external(inner)
×
1155

1156
        raise GeoEngineException({"message": "Unknown DataId type"})
×
1157

1158
    @abstractmethod
1✔
1159
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1160
        pass
×
1161

1162

1163
class InternalDataId(DataId):
1✔
1164
    """An internal data id"""
1165

1166
    __dataset_id: UUID
1✔
1167

1168
    def __init__(self, dataset_id: UUID):
1✔
1169
        self.__dataset_id = dataset_id
1✔
1170

1171
    @classmethod
1✔
1172
    def from_response_internal(cls, response: geoengine_openapi_client.InternalDataId) -> InternalDataId:
1✔
1173
        """Parse an http response to a `InternalDataId` object"""
1174
        return InternalDataId(UUID(response.dataset_id))
1✔
1175

1176
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1177
        return geoengine_openapi_client.DataId(
×
1178
            geoengine_openapi_client.InternalDataId(type="internal", dataset_id=str(self.__dataset_id))
1179
        )
1180

1181
    def __str__(self) -> str:
1✔
1182
        return str(self.__dataset_id)
×
1183

1184
    def __repr__(self) -> str:
1✔
1185
        """Display representation of an internal data id"""
1186
        return str(self)
×
1187

1188
    def __eq__(self, other) -> bool:
1✔
1189
        """Check if two internal data ids are equal"""
1190
        if not isinstance(other, self.__class__):
1✔
1191
            return False
×
1192

1193
        return self.__dataset_id == other.__dataset_id  # pylint: disable=protected-access
1✔
1194

1195

1196
class ExternalDataId(DataId):
1✔
1197
    """An external data id"""
1198

1199
    __provider_id: UUID
1✔
1200
    __layer_id: str
1✔
1201

1202
    def __init__(self, provider_id: UUID, layer_id: str):
1✔
1203
        self.__provider_id = provider_id
×
1204
        self.__layer_id = layer_id
×
1205

1206
    @classmethod
1✔
1207
    def from_response_external(cls, response: geoengine_openapi_client.ExternalDataId) -> ExternalDataId:
1✔
1208
        """Parse an http response to a `ExternalDataId` object"""
1209

1210
        return ExternalDataId(UUID(response.provider_id), response.layer_id)
×
1211

1212
    def to_api_dict(self) -> geoengine_openapi_client.DataId:
1✔
1213
        return geoengine_openapi_client.DataId(
×
1214
            geoengine_openapi_client.ExternalDataId(
1215
                type="external",
1216
                provider_id=str(self.__provider_id),
1217
                layer_id=self.__layer_id,
1218
            )
1219
        )
1220

1221
    def __str__(self) -> str:
1✔
1222
        return f"{self.__provider_id}:{self.__layer_id}"
×
1223

1224
    def __repr__(self) -> str:
1✔
1225
        """Display representation of an external data id"""
1226
        return str(self)
×
1227

1228
    def __eq__(self, other) -> bool:
1✔
1229
        """Check if two external data ids are equal"""
1230
        if not isinstance(other, self.__class__):
×
1231
            return False
×
1232

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

1235

1236
class Measurement:  # pylint: disable=too-few-public-methods
1✔
1237
    """
1238
    Base class for measurements
1239
    """
1240

1241
    @staticmethod
1✔
1242
    def from_response(response: geoengine_openapi_client.Measurement) -> Measurement:
1✔
1243
        """
1244
        Parse a result descriptor from an http response
1245
        """
1246
        inner = response.actual_instance
1✔
1247

1248
        if isinstance(inner, geoengine_openapi_client.UnitlessMeasurement):
1✔
1249
            return UnitlessMeasurement()
1✔
1250
        if isinstance(inner, geoengine_openapi_client.ContinuousMeasurement):
1✔
1251
            return ContinuousMeasurement.from_response_continuous(inner)
1✔
1252
        if isinstance(inner, geoengine_openapi_client.ClassificationMeasurement):
×
1253
            return ClassificationMeasurement.from_response_classification(inner)
×
1254

1255
        raise TypeException("Unknown `Measurement` type")
×
1256

1257
    @abstractmethod
1✔
1258
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1259
        pass
×
1260

1261

1262
class UnitlessMeasurement(Measurement):
1✔
1263
    """A measurement that is unitless"""
1264

1265
    def __str__(self) -> str:
1✔
1266
        """String representation of a unitless measurement"""
1267
        return "unitless"
1✔
1268

1269
    def __repr__(self) -> str:
1✔
1270
        """Display representation of a unitless measurement"""
1271
        return str(self)
×
1272

1273
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1274
        return geoengine_openapi_client.Measurement(geoengine_openapi_client.UnitlessMeasurement(type="unitless"))
1✔
1275

1276

1277
class ContinuousMeasurement(Measurement):
1✔
1278
    """A measurement that is continuous"""
1279

1280
    __measurement: str
1✔
1281
    __unit: str | None
1✔
1282

1283
    def __init__(self, measurement: str, unit: str | None) -> None:
1✔
1284
        """Initialize a new `ContiuousMeasurement`"""
1285

1286
        super().__init__()
1✔
1287

1288
        self.__measurement = measurement
1✔
1289
        self.__unit = unit
1✔
1290

1291
    @staticmethod
1✔
1292
    def from_response_continuous(response: geoengine_openapi_client.ContinuousMeasurement) -> ContinuousMeasurement:
1✔
1293
        """Initialize a new `ContiuousMeasurement from a JSON response"""
1294

1295
        return ContinuousMeasurement(response.measurement, response.unit)
1✔
1296

1297
    def __str__(self) -> str:
1✔
1298
        """String representation of a continuous measurement"""
1299

1300
        if self.__unit is None:
1✔
1301
            return self.__measurement
1✔
1302

1303
        return f"{self.__measurement} ({self.__unit})"
×
1304

1305
    def __repr__(self) -> str:
1✔
1306
        """Display representation of a continuous measurement"""
1307
        return str(self)
×
1308

1309
    def to_api_dict(self) -> geoengine_openapi_client.Measurement:
1✔
1310
        return geoengine_openapi_client.Measurement(
1✔
1311
            geoengine_openapi_client.ContinuousMeasurement(
1312
                type="continuous", measurement=self.__measurement, unit=self.__unit
1313
            )
1314
        )
1315

1316
    @property
1✔
1317
    def measurement(self) -> str:
1✔
1318
        return self.__measurement
×
1319

1320
    @property
1✔
1321
    def unit(self) -> str | None:
1✔
1322
        return self.__unit
×
1323

1324

1325
class ClassificationMeasurement(Measurement):
1✔
1326
    """A measurement that is a classification"""
1327

1328
    __measurement: str
1✔
1329
    __classes: dict[int, str]
1✔
1330

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

1334
        super().__init__()
1✔
1335

1336
        self.__measurement = measurement
1✔
1337
        self.__classes = classes
1✔
1338

1339
    @staticmethod
1✔
1340
    def from_response_classification(
1✔
1341
        response: geoengine_openapi_client.ClassificationMeasurement,
1342
    ) -> ClassificationMeasurement:
1343
        """Initialize a new `ClassificationMeasurement from a JSON response"""
1344

1345
        measurement = response.measurement
×
1346

1347
        str_classes: dict[str, str] = response.classes
×
1348
        classes = {int(k): v for k, v in str_classes.items()}
×
1349

1350
        return ClassificationMeasurement(measurement, classes)
×
1351

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

1355
        return geoengine_openapi_client.Measurement(
1✔
1356
            geoengine_openapi_client.ClassificationMeasurement(
1357
                type="classification", measurement=self.__measurement, classes=str_classes
1358
            )
1359
        )
1360

1361
    def __str__(self) -> str:
1✔
1362
        """String representation of a classification measurement"""
1363
        classes_str = ", ".join(f"{k}: {v}" for k, v in self.__classes.items())
×
1364
        return f"{self.__measurement} ({classes_str})"
×
1365

1366
    def __repr__(self) -> str:
1✔
1367
        """Display representation of a classification measurement"""
1368
        return str(self)
×
1369

1370
    @property
1✔
1371
    def measurement(self) -> str:
1✔
1372
        return self.__measurement
×
1373

1374
    @property
1✔
1375
    def classes(self) -> dict[int, str]:
1✔
1376
        return self.__classes
×
1377

1378

1379
class GeoTransform:
1✔
1380
    """The `GeoTransform` specifies the relationship between pixel coordinates and geographic coordinates."""
1381

1382
    x_min: float
1✔
1383
    y_max: float
1✔
1384
    """In Geo Engine, x_pixel_size is always positive."""
1✔
1385
    x_pixel_size: float
1✔
1386
    """In Geo Engine, y_pixel_size is always negative."""
1✔
1387
    y_pixel_size: float
1✔
1388

1389
    def __init__(self, x_min: float, y_max: float, x_pixel_size: float, y_pixel_size: float):
1✔
1390
        """Initialize a new `GeoTransform`"""
1391

1392
        assert x_pixel_size > 0, "In Geo Engine, x_pixel_size is always positive."
1✔
1393
        assert y_pixel_size < 0, "In Geo Engine, y_pixel_size is always negative."
1✔
1394

1395
        self.x_min = x_min
1✔
1396
        self.y_max = y_max
1✔
1397
        self.x_pixel_size = x_pixel_size
1✔
1398
        self.y_pixel_size = y_pixel_size
1✔
1399

1400
    @classmethod
1✔
1401
    def from_response(cls, response: geoengine_openapi_client.GdalDatasetGeoTransform) -> GeoTransform:
1✔
1402
        """Parse a geotransform from an HTTP JSON response"""
1403

1404
        return GeoTransform(
1✔
1405
            x_min=response.origin_coordinate.x,
1406
            y_max=response.origin_coordinate.y,
1407
            x_pixel_size=response.x_pixel_size,
1408
            y_pixel_size=response.y_pixel_size,
1409
        )
1410

1411
    def to_api_dict(self) -> geoengine_openapi_client.GdalDatasetGeoTransform:
1✔
1412
        return geoengine_openapi_client.GdalDatasetGeoTransform(
1✔
1413
            origin_coordinate=geoengine_openapi_client.Coordinate2D(
1414
                x=self.x_min,
1415
                y=self.y_max,
1416
            ),
1417
            x_pixel_size=self.x_pixel_size,
1418
            y_pixel_size=self.y_pixel_size,
1419
        )
1420

1421
    def to_gdal(self) -> tuple[float, float, float, float, float, float]:
1✔
1422
        """Convert to a GDAL geotransform"""
1423
        return (self.x_min, self.x_pixel_size, 0, self.y_max, 0, self.y_pixel_size)
×
1424

1425
    def __str__(self) -> str:
1✔
1426
        return (
×
1427
            f"Origin: ({self.x_min}, {self.y_max}), "
1428
            f"X Pixel Size: {self.x_pixel_size}, "
1429
            f"Y Pixel Size: {self.y_pixel_size}"
1430
        )
1431

1432
    def __repr__(self) -> str:
1✔
1433
        return str(self)
×
1434

1435
    @property
1✔
1436
    def x_half_pixel_size(self) -> float:
1✔
1437
        return self.x_pixel_size / 2.0
1✔
1438

1439
    @property
1✔
1440
    def y_half_pixel_size(self) -> float:
1✔
1441
        return self.y_pixel_size / 2.0
1✔
1442

1443
    def x_max(self, number_of_pixels: int) -> float:
1✔
1444
        return self.x_min + number_of_pixels * self.x_pixel_size
1✔
1445

1446
    def y_min(self, number_of_pixels: int) -> float:
1✔
1447
        return self.y_max + number_of_pixels * self.y_pixel_size
1✔
1448

1449
    def coord_to_pixel_ul(self, x_cord: float, y_coord: float) -> tuple[int, int]:
1✔
1450
        """Convert a coordinate to a pixel index rould towards top left"""
1451
        return (
×
1452
            int(np.floor((x_cord - self.x_min) / self.x_pixel_size)),
1453
            int(np.ceil((y_coord - self.y_max) / self.y_pixel_size)),
1454
        )
1455

1456
    def coord_to_pixel_lr(self, x_cord: float, y_coord: float) -> tuple[int, int]:
1✔
1457
        """Convert a coordinate to a pixel index ound towards lower right"""
1458
        return (
×
1459
            int(np.ceil((x_cord - self.x_min) / self.x_pixel_size)),
1460
            int(np.floor((y_coord - self.y_max) / self.y_pixel_size)),
1461
        )
1462

1463
    def spatial_resolution(self) -> SpatialResolution:
1✔
1464
        return SpatialResolution(x_resolution=abs(self.x_pixel_size), y_resolution=abs(self.y_pixel_size))
×
1465

1466
    def __eq__(self, other) -> bool:
1✔
1467
        """Check if two geotransforms are equal"""
1468
        if not isinstance(other, GeoTransform):
×
1469
            return False
×
1470

1471
        return (
×
1472
            self.x_min == other.x_min
1473
            and self.y_max == other.y_max
1474
            and self.x_pixel_size == other.x_pixel_size
1475
            and self.y_pixel_size == other.y_pixel_size
1476
        )
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