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

geo-engine / geoengine-python / 14441260704

14 Apr 2025 08:40AM UTC coverage: 75.452% (-1.2%) from 76.67%
14441260704

Pull #221

github

web-flow
Merge a9db07509 into 89c260aaf
Pull Request #221: Pixel_based_queries_rewrite

2837 of 3760 relevant lines covered (75.45%)

0.75 hits per line

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

66.16
geoengine/workflow_builder/operators.py
1
'''This module contains helpers to create workflow operators for the Geo Engine API.'''
2
from __future__ import annotations
1✔
3

4
from abc import abstractmethod
1✔
5
from dataclasses import dataclass
1✔
6
from enum import Enum
1✔
7
from typing import Any, Dict, List, Optional, Tuple, Union, cast, Literal
1✔
8
import datetime
1✔
9
import geoengine_openapi_client
1✔
10
import numpy as np
1✔
11

12
from geoengine.datasets import DatasetName
1✔
13
from geoengine.types import Measurement, RasterBandDescriptor
1✔
14

15
# pylint: disable=too-many-lines
16

17

18
class Operator():
1✔
19
    '''Base class for all operators.'''
20

21
    @abstractmethod
1✔
22
    def name(self) -> str:
1✔
23
        '''Returns the name of the operator.'''
24

25
    @abstractmethod
1✔
26
    def to_dict(self) -> Dict[str, Any]:
1✔
27
        '''Returns a dictionary representation of the operator that can be used to create a JSON request for the API.'''
28

29
    @abstractmethod
1✔
30
    def data_type(self) -> Literal['Raster', 'Vector']:
1✔
31
        '''Returns the type of the operator.'''
32

33
    def to_workflow_dict(self) -> Dict[str, Any]:
1✔
34
        '''Returns a dictionary representation of a workflow that calls the operator" \
35
             "that can be used to create a JSON request for the workflow API.'''
36

37
        return {
1✔
38
            'type': self.data_type(),
39
            'operator': self.to_dict(),
40
        }
41

42
    @classmethod
1✔
43
    def from_workflow_dict(cls, workflow) -> Operator:
1✔
44
        '''Returns an operator from a workflow dictionary.'''
45
        if workflow['type'] == 'Raster':
1✔
46
            return RasterOperator.from_operator_dict(workflow['operator'])
1✔
47
        if workflow['type'] == 'Vector':
×
48
            return VectorOperator.from_operator_dict(workflow['operator'])
×
49

50
        raise NotImplementedError(f"Unknown workflow type {workflow['type']}")
×
51

52

53
class RasterOperator(Operator):
1✔
54
    '''Base class for all raster operators.'''
55

56
    @abstractmethod
1✔
57
    def to_dict(self) -> Dict[str, Any]:
1✔
58
        pass
×
59

60
    def data_type(self) -> Literal['Raster', 'Vector']:
1✔
61
        return 'Raster'
1✔
62

63
    @classmethod
1✔
64
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> RasterOperator:  # pylint: disable=too-many-return-statements
1✔
65
        '''Returns an operator from a dictionary.'''
66
        if operator_dict['type'] == 'GdalSource':
1✔
67
            return GdalSource.from_operator_dict(operator_dict)
1✔
68
        if operator_dict['type'] == 'RasterScaling':
1✔
69
            return RasterScaling.from_operator_dict(operator_dict)
1✔
70
        if operator_dict['type'] == 'RasterTypeConversion':
1✔
71
            return RasterTypeConversion.from_operator_dict(operator_dict)
1✔
72
        if operator_dict['type'] == 'Reprojection':
×
73
            return Reprojection.from_operator_dict(operator_dict).as_raster()
×
74
        if operator_dict['type'] == 'Interpolation':
×
75
            return Interpolation.from_operator_dict(operator_dict)
×
76
        if operator_dict['type'] == 'Expression':
×
77
            return Expression.from_operator_dict(operator_dict)
×
78
        if operator_dict['type'] == 'BandwiseExpression':
×
79
            return BandwiseExpression.from_operator_dict(operator_dict)
×
80
        if operator_dict['type'] == 'TimeShift':
×
81
            return TimeShift.from_operator_dict(operator_dict).as_raster()
×
82
        if operator_dict['type'] == 'TemporalRasterAggregation':
×
83
            return TemporalRasterAggregation.from_operator_dict(operator_dict)
×
84
        if operator_dict['type'] == 'RasterStacker':
×
85
            return RasterStacker.from_operator_dict(operator_dict)
×
86
        if operator_dict['type'] == 'BandNeighborhoodAggregate':
×
87
            return BandNeighborhoodAggregate.from_operator_dict(operator_dict)
×
88

89
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
90

91

92
class VectorOperator(Operator):
1✔
93
    '''Base class for all vector operators.'''
94

95
    @abstractmethod
1✔
96
    def to_dict(self) -> Dict[str, Any]:
1✔
97
        pass
×
98

99
    def data_type(self) -> Literal['Raster', 'Vector']:
1✔
100
        return 'Vector'
×
101

102
    @classmethod
1✔
103
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> VectorOperator:
1✔
104
        '''Returns an operator from a dictionary.'''
105
        if operator_dict['type'] == 'OgrSource':
1✔
106
            return OgrSource.from_operator_dict(operator_dict)
1✔
107
        if operator_dict['type'] == 'Reprojection':
×
108
            return Reprojection.from_operator_dict(operator_dict).as_vector()
×
109
        if operator_dict['type'] == 'RasterVectorJoin':
×
110
            return RasterVectorJoin.from_operator_dict(operator_dict)
×
111
        if operator_dict['type'] == 'PointInPolygonFilter':
×
112
            return PointInPolygonFilter.from_operator_dict(operator_dict)
×
113
        if operator_dict['type'] == 'TimeShift':
×
114
            return TimeShift.from_operator_dict(operator_dict).as_vector()
×
115
        if operator_dict['type'] == 'VectorExpression':
×
116
            return VectorExpression.from_operator_dict(operator_dict)
×
117
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
118

119

120
class GdalSource(RasterOperator):
1✔
121
    '''A GDAL source operator.'''
122
    dataset: str
1✔
123

124
    def __init__(self, dataset: Union[str, DatasetName]):
1✔
125
        '''Creates a new GDAL source operator.'''
126
        if isinstance(dataset, DatasetName):
1✔
127
            dataset = str(dataset)
1✔
128
        self.dataset = dataset
1✔
129

130
    def name(self) -> str:
1✔
131
        return 'GdalSource'
1✔
132

133
    def to_dict(self) -> Dict[str, Any]:
1✔
134
        return {
1✔
135
            'type': self.name(),
136
            'params': {
137
                "data": self.dataset
138
            }
139
        }
140

141
    @classmethod
1✔
142
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> GdalSource:
1✔
143
        '''Returns an operator from a dictionary.'''
144
        if operator_dict["type"] != "GdalSource":
1✔
145
            raise ValueError("Invalid operator type")
×
146

147
        return GdalSource(cast(str, operator_dict['params']['data']))
1✔
148

149

150
class OgrSource(VectorOperator):
1✔
151
    '''An OGR source operator.'''
152
    dataset: str
1✔
153
    attribute_projection: Optional[str] = None
1✔
154
    attribute_filters: Optional[str] = None
1✔
155

156
    def __init__(
1✔
157
            self,
158
            dataset: Union[str, DatasetName],
159
            attribute_projection: Optional[str] = None,
160
            attribute_filters: Optional[str] = None
161
    ):
162
        '''Creates a new OGR source operator.'''
163
        if isinstance(dataset, DatasetName):
1✔
164
            dataset = str(dataset)
×
165
        self.dataset = dataset
1✔
166
        self.attribute_projection = attribute_projection
1✔
167
        self.attribute_filters = attribute_filters
1✔
168

169
    def name(self) -> str:
1✔
170
        return 'OgrSource'
1✔
171

172
    def to_dict(self) -> Dict[str, Any]:
1✔
173
        return {
1✔
174
            'type': self.name(),
175
            'params': {
176
                "data": self.dataset,
177
                'attributeProjection': self.attribute_projection,
178
                'attributeFilters': self.attribute_filters,
179
            }
180
        }
181

182
    @classmethod
1✔
183
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> OgrSource:
1✔
184
        '''Returns an operator from a dictionary.'''
185
        if operator_dict["type"] != "OgrSource":
1✔
186
            raise ValueError("Invalid operator type")
×
187

188
        params = operator_dict['params']
1✔
189
        return OgrSource(
1✔
190
            cast(str, params['data']),
191
            attribute_projection=cast(Optional[str], params.get('attributeProjection')),
192
            attribute_filters=cast(Optional[str], params.get('attributeFilters')),
193
        )
194

195

196
class Interpolation(RasterOperator):
1✔
197
    '''An interpolation operator.'''
198
    source: RasterOperator
1✔
199
    interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear"
1✔
200
    output_method: Literal["resolution", "fraction"] = "resolution"
1✔
201
    output_x: float
1✔
202
    output_y: float
1✔
203
    # outputOriginReference: Optional[Coordinate2D]
204

205
    def __init__(
1✔
206
        self,
207
        source_operator: RasterOperator,
208
        output_x: float,
209
        output_y: float,
210
        output_method: Literal["resolution", "fraction"] = "resolution",
211
        interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear",
212

213
    ):
214
        # pylint: disable=too-many-arguments,too-many-positional-arguments
215
        '''Creates a new interpolation operator.'''
216
        self.source = source_operator
1✔
217
        self.interpolation = interpolation
1✔
218
        self.output_method = output_method
1✔
219
        self.output_x = output_x
1✔
220
        self.output_y = output_y
1✔
221

222
    def name(self) -> str:
1✔
223
        return 'Interpolation'
1✔
224

225
    def to_dict(self) -> Dict[str, Any]:
1✔
226

227
        if self.output_method == 'fraction':
1✔
228
            input_res = {
1✔
229
                "type": "fraction",
230
                "x": self.output_x,
231
                "y": self.output_y
232
            }
233
        else:
234
            input_res = {
×
235
                "type": "resolution",
236
                "x": self.output_x,
237
                "y": self.output_y
238
            }
239

240
        return {
1✔
241
            "type": self.name(),
242
            "params": {
243
                "interpolation": self.interpolation,
244
                "outputResolution": input_res
245
            },
246
            "sources": {
247
                "raster": self.source.to_dict()
248
            }
249
        }
250

251
    @classmethod
1✔
252
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> Interpolation:
1✔
253
        '''Returns an operator from a dictionary.'''
254
        if operator_dict["type"] != "Interpolation":
1✔
255
            raise ValueError("Invalid operator type")
×
256

257
        source = RasterOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['raster']))
1✔
258

259
        def parse_input_params(params: Dict[str, Any]) -> Tuple[Literal["resolution", "fraction"], float, float]:
1✔
260
            output_x = float(params['x'])
1✔
261
            output_y = float(params['y'])
1✔
262

263
            if 'type' not in params:
1✔
264
                raise KeyError("Interpolation outputResolution must contain a type: resolution OR fraction")
×
265
            if params['type'] == 'fraction':
1✔
266
                return ('fraction', output_x, output_y)
1✔
267
            if params['type'] == 'fraction':
×
268
                return ('fraction', output_x, output_y)
×
269
            raise ValueError(f"Invalid interpolation outputResolution type {params['type']}")
×
270

271
        (output_method, output_x, output_y) = parse_input_params(
1✔
272
            cast(Dict[str, Any], operator_dict['params']['outputResolution'])
273
        )
274

275
        return Interpolation(
1✔
276
            source_operator=source,
277
            output_method=output_method,
278
            output_x=output_x,
279
            output_y=output_y,
280
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict['params']['interpolation'])
281
        )
282

283

284
class Downsampling(RasterOperator):
1✔
285
    '''A Downsampling operator.'''
286
    source: RasterOperator
1✔
287
    sample_method: Literal["nearestNeighbor"] = "nearestNeighbor"
1✔
288
    output_method: Literal["resolution", "fraction"] = "resolution"
1✔
289
    output_x: float
1✔
290
    output_y: float
1✔
291
    # outputOriginReference: Optional[Coordinate2D]
292

293
    def __init__(
1✔
294
        self,
295
        source_operator: RasterOperator,
296
        output_x: float,
297
        output_y: float,
298
        output_method: Literal["resolution", "fraction"] = "resolution",
299
        sample_method: Literal["nearestNeighbor"] = "nearestNeighbor",
300

301
    ):
302
        # pylint: disable=too-many-arguments,too-many-positional-arguments
303
        '''Creates a new Downsampling operator.'''
304
        self.source = source_operator
×
305
        self.sample_method = sample_method
×
306
        self.output_method = output_method
×
307
        self.output_x = output_x
×
308
        self.output_y = output_y
×
309

310
    def name(self) -> str:
1✔
311
        return 'Downsampling'
×
312

313
    def to_dict(self) -> Dict[str, Any]:
1✔
314

315
        if self.output_method == 'fraction':
×
316
            input_res = {
×
317
                "type": "fraction",
318
                "x": self.output_x,
319
                "y": self.output_y
320
            }
321
        else:
322
            input_res = {
×
323
                "type": "resolution",
324
                "x": self.output_x,
325
                "y": self.output_y
326
            }
327

328
        return {
×
329
            "type": self.name(),
330
            "params": {
331
                "samplingMethod": self.sample_method,
332
                "outputResolution": input_res
333
            },
334
            "sources": {
335
                "raster": self.source.to_dict()
336
            }
337
        }
338

339
    @classmethod
1✔
340
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> Downsampling:
1✔
341
        '''Returns an operator from a dictionary.'''
342
        if operator_dict["type"] != "Downsampling":
×
343
            raise ValueError("Invalid operator type")
×
344

345
        source = RasterOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['raster']))
×
346

347
        def parse_input_params(params: Dict[str, Any]) -> Tuple[Literal["resolution", "fraction"], float, float]:
×
348
            output_x = float(params['x'])
×
349
            output_y = float(params['y'])
×
350

351
            if 'type' not in params:
×
352
                raise KeyError("Downsampling outputResolution must contain a type: resolution OR fraction")
×
353
            if params['type'] == 'fraction':
×
354
                return ('fraction', output_x, output_y)
×
355
            if params['type'] == 'fraction':
×
356
                return ('fraction', output_x, output_y)
×
357
            raise ValueError(f"Invalid Downsampling outputResolution type {params['type']}")
×
358

359
        (output_method, output_x, output_y) = parse_input_params(
×
360
            cast(Dict[str, Any], operator_dict['params']['outputResolution'])
361
        )
362

363
        return Downsampling(
×
364
            source_operator=source,
365
            output_method=output_method,
366
            output_x=output_x,
367
            output_y=output_y,
368
            sample_method=cast(Literal["nearestNeighbor"], operator_dict['params']['downsampling'])
369
        )
370

371

372
class ColumnNames:
1✔
373
    '''Base class for deriving column names from bands of a raster.'''
374

375
    @abstractmethod
1✔
376
    def to_dict(self) -> Dict[str, Any]:
1✔
377
        pass
×
378

379
    @classmethod
1✔
380
    def from_dict(cls, rename_dict: Dict[str, Any]) -> 'ColumnNames':
1✔
381
        '''Returns a ColumnNames object from a dictionary.'''
382
        if rename_dict["type"] == "default":
1✔
383
            return ColumnNamesDefault()
×
384
        if rename_dict["type"] == "suffix":
1✔
385
            return ColumnNamesSuffix(cast(List[str], rename_dict["values"]))
×
386
        if rename_dict["type"] == "names":
1✔
387
            return ColumnNamesNames(cast(List[str], rename_dict["values"]))
1✔
388
        raise ValueError("Invalid rename type")
×
389

390
    @classmethod
1✔
391
    def default(cls) -> 'ColumnNames':
1✔
392
        return ColumnNamesDefault()
×
393

394
    @classmethod
1✔
395
    def suffix(cls, values: List[str]) -> 'ColumnNames':
1✔
396
        return ColumnNamesSuffix(values)
×
397

398
    @classmethod
1✔
399
    def rename(cls, values: List[str]) -> 'ColumnNames':
1✔
400
        return ColumnNamesNames(values)
×
401

402

403
class ColumnNamesDefault(ColumnNames):
1✔
404
    '''column names with default suffix.'''
405

406
    def to_dict(self) -> Dict[str, Any]:
1✔
407
        return {
×
408
            "type": "default"
409
        }
410

411

412
class ColumnNamesSuffix(ColumnNames):
1✔
413
    '''Rename bands with custom suffixes.'''
414

415
    suffixes: List[str]
1✔
416

417
    def __init__(self, suffixes: List[str]) -> None:
1✔
418
        self.suffixes = suffixes
×
419
        super().__init__()
×
420

421
    def to_dict(self) -> Dict[str, Any]:
1✔
422
        return {
×
423
            "type": "suffix",
424
            "values": self.suffixes
425
        }
426

427

428
class ColumnNamesNames(ColumnNames):
1✔
429
    '''Rename bands with new names.'''
430

431
    new_names: List[str]
1✔
432

433
    def __init__(self, new_names: List[str]) -> None:
1✔
434
        self.new_names = new_names
1✔
435
        super().__init__()
1✔
436

437
    def to_dict(self) -> Dict[str, Any]:
1✔
438
        return {
1✔
439
            "type": "names",
440
            "values": self.new_names
441
        }
442

443

444
class RasterVectorJoin(VectorOperator):
1✔
445
    '''A RasterVectorJoin operator.'''
446
    raster_sources: List[RasterOperator]
1✔
447
    vector_source: VectorOperator
1✔
448
    names: ColumnNames
1✔
449
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1✔
450
    temporal_aggregation_ignore_nodata: bool = False
1✔
451
    feature_aggregation: Literal["first", "mean"] = "mean"
1✔
452
    feature_aggregation_ignore_nodata: bool = False
1✔
453

454
    # pylint: disable=too-many-arguments,too-many-positional-arguments
455
    def __init__(self,
1✔
456
                 raster_sources: List[RasterOperator],
457
                 vector_source: VectorOperator,
458
                 names: ColumnNames,
459
                 temporal_aggregation: Literal["none", "first", "mean"] = "none",
460
                 temporal_aggregation_ignore_nodata: bool = False,
461
                 feature_aggregation: Literal["first", "mean"] = "mean",
462
                 feature_aggregation_ignore_nodata: bool = False,
463
                 ):
464
        '''Creates a new RasterVectorJoin operator.'''
465
        self.raster_source = raster_sources
1✔
466
        self.vector_source = vector_source
1✔
467
        self.names = names
1✔
468
        self.temporal_aggregation = temporal_aggregation
1✔
469
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1✔
470
        self.feature_aggregation = feature_aggregation
1✔
471
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1✔
472

473
    def name(self) -> str:
1✔
474
        return 'RasterVectorJoin'
1✔
475

476
    def to_dict(self) -> Dict[str, Any]:
1✔
477
        return {
1✔
478
            "type": self.name(),
479
            "params": {
480
                "names": self.names.to_dict(),
481
                "temporalAggregation": self.temporal_aggregation,
482
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
483
                "featureAggregation": self.feature_aggregation,
484
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
485
            },
486
            "sources": {
487
                "vector": self.vector_source.to_dict(),
488
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source]
489
            }
490
        }
491

492
    @classmethod
1✔
493
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterVectorJoin':
1✔
494
        '''Returns an operator from a dictionary.'''
495
        if operator_dict["type"] != "RasterVectorJoin":
1✔
496
            raise ValueError("Invalid operator type")
×
497

498
        vector_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['vector']))
1✔
499
        raster_sources = [
1✔
500
            RasterOperator.from_operator_dict(raster_source) for raster_source in cast(
501
                List[Dict[str, Any]], operator_dict['sources']['rasters']
502
            )
503
        ]
504

505
        params = operator_dict['params']
1✔
506
        return RasterVectorJoin(
1✔
507
            raster_sources=raster_sources,
508
            vector_source=vector_source,
509
            names=ColumnNames.from_dict(params['names']),
510
            temporal_aggregation=cast(Literal["none", "first", "mean"], params['temporalAggregation']),
511
            temporal_aggregation_ignore_nodata=cast(bool, params['temporalAggregationIgnoreNoData']),
512
            feature_aggregation=cast(Literal["first", "mean"], params['featureAggregation']),
513
            feature_aggregation_ignore_nodata=cast(bool, params['featureAggregationIgnoreNoData']),
514
        )
515

516

517
class PointInPolygonFilter(VectorOperator):
1✔
518
    '''A PointInPolygonFilter operator.'''
519

520
    point_source: VectorOperator
1✔
521
    polygon_source: VectorOperator
1✔
522

523
    def __init__(self,
1✔
524
                 point_source: VectorOperator,
525
                 polygon_source: VectorOperator,
526
                 ):
527
        '''Creates a new PointInPolygonFilter filter operator.'''
528
        self.point_source = point_source
1✔
529
        self.polygon_source = polygon_source
1✔
530

531
    def name(self) -> str:
1✔
532
        return 'PointInPolygonFilter'
1✔
533

534
    def to_dict(self) -> Dict[str, Any]:
1✔
535
        return {
1✔
536
            "type": self.name(),
537
            "params": {},
538
            "sources": {
539
                "points": self.point_source.to_dict(),
540
                "polygons": self.polygon_source.to_dict()
541
            }
542
        }
543

544
    @classmethod
1✔
545
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> PointInPolygonFilter:
1✔
546
        '''Returns an operator from a dictionary.'''
547
        if operator_dict["type"] != "PointInPolygonFilter":
1✔
548
            raise ValueError("Invalid operator type")
×
549

550
        point_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['points']))
1✔
551
        polygon_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['polygons']))
1✔
552

553
        return PointInPolygonFilter(
1✔
554
            point_source=point_source,
555
            polygon_source=polygon_source,
556
        )
557

558

559
class RasterScaling(RasterOperator):
1✔
560
    '''A RasterScaling operator.
561

562
    This operator scales the values of a raster by a given slope and offset.
563

564
    The scaling is done as follows:
565
    y = (x - offset) / slope
566

567
    The unscale mode is the inverse of the scale mode:
568
    x = y * slope + offset
569

570
    '''
571

572
    source: RasterOperator
1✔
573
    slope: Optional[Union[float, str]] = None
1✔
574
    offset: Optional[Union[float, str]] = None
1✔
575
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1✔
576
    output_measurement: Optional[str] = None
1✔
577

578
    def __init__(self,
1✔
579
                 # pylint: disable=too-many-arguments,too-many-positional-arguments
580
                 source: RasterOperator,
581
                 slope: Optional[Union[float, str]] = None,
582
                 offset: Optional[Union[float, str]] = None,
583
                 scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
584
                 output_measurement: Optional[str] = None
585
                 ):
586
        '''Creates a new RasterScaling operator.'''
587
        self.source = source
1✔
588
        self.slope = slope
1✔
589
        self.offset = offset
1✔
590
        self.scaling_mode = scaling_mode
1✔
591
        self.output_measurement = output_measurement
1✔
592
        if output_measurement is not None:
1✔
593
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
594

595
    def name(self) -> str:
1✔
596
        return 'RasterScaling'
1✔
597

598
    def to_dict(self) -> Dict[str, Any]:
1✔
599
        def offset_scale_dict(key_or_value: Optional[Union[float, str]]) -> Dict[str, Any]:
1✔
600
            if key_or_value is None:
1✔
601
                return {"type": "auto"}
1✔
602

603
            if isinstance(key_or_value, float):
1✔
604
                return {"type": "constant", "value": key_or_value}
1✔
605

606
            if isinstance(key_or_value, int):
×
607
                return {"type": "constant", "value": float(key_or_value)}
×
608

609
            # TODO: incorporate `domain` field
610
            return {"type": "metadataKey", "key": key_or_value}
×
611

612
        return {
1✔
613
            "type": self.name(),
614
            "params": {
615
                "offset": offset_scale_dict(self.offset),
616
                "slope": offset_scale_dict(self.slope),
617
                "scalingMode": self.scaling_mode
618
            },
619
            "sources": {
620
                "raster": self.source.to_dict()
621
            }
622
        }
623

624
    @classmethod
1✔
625
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterScaling':
1✔
626
        if operator_dict["type"] != "RasterScaling":
1✔
627
            raise ValueError("Invalid operator type")
×
628

629
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
630
        params = operator_dict["params"]
1✔
631

632
        def offset_slope_reverse(key_or_value: Optional[Dict[str, Any]]) -> Optional[Union[float, str]]:
1✔
633
            if key_or_value is None:
1✔
634
                return None
×
635
            if key_or_value["type"] == "constant":
1✔
636
                return key_or_value["value"]
1✔
637
            if key_or_value["type"] == "metadataKey":
1✔
638
                return key_or_value["key"]
×
639
            return None
1✔
640

641
        return RasterScaling(
1✔
642
            source_operator,
643
            slope=offset_slope_reverse(params["slope"]),
644
            offset=offset_slope_reverse(params["offset"]),
645
            scaling_mode=params["scalingMode"],
646
            output_measurement=params.get("outputMeasurement", None)
647
        )
648

649

650
class RasterTypeConversion(RasterOperator):
1✔
651
    '''A RasterTypeConversion operator.'''
652

653
    source: RasterOperator
1✔
654
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
655

656
    def __init__(self,
1✔
657
                 source: RasterOperator,
658
                 output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
659
                 ):
660
        '''Creates a new RasterTypeConversion operator.'''
661
        self.source = source
1✔
662
        self.output_data_type = output_data_type
1✔
663

664
    def name(self) -> str:
1✔
665
        return 'RasterTypeConversion'
1✔
666

667
    def to_dict(self) -> Dict[str, Any]:
1✔
668
        return {
1✔
669
            "type": self.name(),
670
            "params": {
671
                "outputDataType": self.output_data_type
672
            },
673
            "sources": {
674
                "raster": self.source.to_dict()
675
            }
676
        }
677

678
    @classmethod
1✔
679
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterTypeConversion':
1✔
680
        if operator_dict["type"] != "RasterTypeConversion":
1✔
681
            raise ValueError("Invalid operator type")
×
682

683
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
684

685
        return RasterTypeConversion(
1✔
686
            source_operator,
687
            output_data_type=operator_dict["params"]["outputDataType"]
688
        )
689

690

691
class Reprojection(Operator):
1✔
692
    '''A Reprojection operator.'''
693
    source: Operator
1✔
694
    target_spatial_reference: str
1✔
695

696
    def __init__(self,
1✔
697
                 source: Operator,
698
                 target_spatial_reference: str
699
                 ):
700
        '''Creates a new Reprojection operator.'''
701
        self.source = source
1✔
702
        self.target_spatial_reference = target_spatial_reference
1✔
703

704
    def data_type(self) -> Literal['Raster', 'Vector']:
1✔
705
        return self.source.data_type()
×
706

707
    def name(self) -> str:
1✔
708
        return 'Reprojection'
1✔
709

710
    def to_dict(self) -> Dict[str, Any]:
1✔
711
        return {
1✔
712
            "type": self.name(),
713
            "params": {
714
                "targetSpatialReference": self.target_spatial_reference
715
            },
716
            "sources": {
717
                "source": self.source.to_dict()
718
            }
719
        }
720

721
    def as_vector(self) -> VectorOperator:
1✔
722
        '''Casts this operator to a VectorOperator.'''
723
        if self.data_type() != 'Vector':
×
724
            raise TypeError("Cannot cast to VectorOperator")
×
725
        return cast(VectorOperator, self)
×
726

727
    def as_raster(self) -> RasterOperator:
1✔
728
        '''Casts this operator to a RasterOperator.'''
729
        if self.data_type() != 'Raster':
×
730
            raise TypeError("Cannot cast to RasterOperator")
×
731
        return cast(RasterOperator, self)
×
732

733
    @classmethod
1✔
734
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'Reprojection':
1✔
735
        '''Constructs the operator from the given dictionary.'''
736
        if operator_dict["type"] != "Reprojection":
1✔
737
            raise ValueError("Invalid operator type")
×
738

739
        source_operator: Union[RasterOperator, VectorOperator]
740
        try:
1✔
741
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1✔
742
        except ValueError:
×
743
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
744

745
        return Reprojection(
1✔
746
            source=cast(Operator, source_operator),
747
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"]
748
        )
749

750

751
class Expression(RasterOperator):
1✔
752
    '''An Expression operator.'''
753

754
    expression: str
1✔
755
    source: RasterOperator
1✔
756
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
757
    map_no_data: bool = False
1✔
758
    output_band: Optional[RasterBandDescriptor] = None
1✔
759

760
    # pylint: disable=too-many-arguments,too-many-positional-arguments
761
    def __init__(self,
1✔
762
                 expression: str,
763
                 source: RasterOperator,
764
                 output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
765
                 map_no_data: bool = False,
766
                 output_band: Optional[RasterBandDescriptor] = None,
767
                 ):
768
        '''Creates a new Expression operator.'''
769
        self.expression = expression
1✔
770
        self.source = source
1✔
771
        self.output_type = output_type
1✔
772
        self.map_no_data = map_no_data
1✔
773
        self.output_band = output_band
1✔
774

775
    def name(self) -> str:
1✔
776
        return 'Expression'
1✔
777

778
    def to_dict(self) -> Dict[str, Any]:
1✔
779
        params = {
1✔
780
            "expression": self.expression,
781
            "outputType": self.output_type,
782
            "mapNoData": self.map_no_data,
783
        }
784
        if self.output_band is not None:
1✔
785
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1✔
786

787
        return {
1✔
788
            "type": self.name(),
789
            "params": params,
790
            "sources": {
791
                "raster": self.source.to_dict()
792
            }
793
        }
794

795
    @classmethod
1✔
796
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'Expression':
1✔
797
        if operator_dict["type"] != "Expression":
1✔
798
            raise ValueError("Invalid operator type")
×
799

800
        output_band = None
1✔
801
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1✔
802
            raster_band_descriptor = geoengine_openapi_client.RasterBandDescriptor.from_dict(
1✔
803
                operator_dict["params"]["outputBand"]
804
            )
805
            if raster_band_descriptor is None:
1✔
806
                raise ValueError("Invalid output band")
×
807
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1✔
808

809
        return Expression(
1✔
810
            expression=operator_dict["params"]["expression"],
811
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
812
            output_type=operator_dict["params"]["outputType"],
813
            map_no_data=operator_dict["params"]["mapNoData"],
814
            output_band=output_band
815
        )
816

817

818
class BandwiseExpression(RasterOperator):
1✔
819
    '''A bandwise Expression operator.'''
820

821
    expression: str
1✔
822
    source: RasterOperator
1✔
823
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
824
    map_no_data: bool = False
1✔
825

826
    # pylint: disable=too-many-arguments
827
    def __init__(self,
1✔
828
                 expression: str,
829
                 source: RasterOperator,
830
                 output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
831
                 map_no_data: bool = False,
832
                 ):
833
        '''Creates a new Expression operator.'''
834
        self.expression = expression
×
835
        self.source = source
×
836
        self.output_type = output_type
×
837
        self.map_no_data = map_no_data
×
838

839
    def name(self) -> str:
1✔
840
        return 'BandwiseExpression'
×
841

842
    def to_dict(self) -> Dict[str, Any]:
1✔
843
        params = {
×
844
            "expression": self.expression,
845
            "outputType": self.output_type,
846
            "mapNoData": self.map_no_data,
847
        }
848

849
        return {
×
850
            "type": self.name(),
851
            "params": params,
852
            "sources": {
853
                "raster": self.source.to_dict()
854
            }
855
        }
856

857
    @classmethod
1✔
858
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'BandwiseExpression':
1✔
859
        if operator_dict["type"] != "BandwiseExpression":
×
860
            raise ValueError("Invalid operator type")
×
861

862
        return BandwiseExpression(
×
863
            expression=operator_dict["params"]["expression"],
864
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
865
            output_type=operator_dict["params"]["outputType"],
866
            map_no_data=operator_dict["params"]["mapNoData"],
867
        )
868

869

870
class GeoVectorDataType(Enum):
1✔
871
    '''The output type of geometry vector data.'''
872
    MULTI_POINT = "MultiPoint"
1✔
873
    MULTI_LINE_STRING = "MultiLineString"
1✔
874
    MULTI_POLYGON = "MultiPolygon"
1✔
875

876

877
class VectorExpression(VectorOperator):
1✔
878
    '''The `VectorExpression` operator.'''
879

880
    source: VectorOperator
1✔
881

882
    expression: str
1✔
883
    input_columns: List[str]
1✔
884
    output_column: str | GeoVectorDataType
1✔
885
    geometry_column_name = None
1✔
886
    output_measurement: Optional[Measurement] = None
1✔
887

888
    # pylint: disable=too-many-arguments
889
    def __init__(self,
1✔
890
                 source: VectorOperator,
891
                 *,
892
                 expression: str,
893
                 input_columns: List[str],
894
                 output_column: str | GeoVectorDataType,
895
                 geometry_column_name: Optional[str] = None,
896
                 output_measurement: Optional[Measurement] = None,
897
                 ):
898
        '''Creates a new VectorExpression operator.'''
899
        self.source = source
×
900

901
        self.expression = expression
×
902
        self.input_columns = input_columns
×
903
        self.output_column = output_column
×
904

905
        self.geometry_column_name = geometry_column_name
×
906
        self.output_measurement = output_measurement
×
907

908
    def name(self) -> str:
1✔
909
        return 'VectorExpression'
×
910

911
    def to_dict(self) -> Dict[str, Any]:
1✔
912
        output_column_dict = None
×
913
        if isinstance(self.output_column, GeoVectorDataType):
×
914
            output_column_dict = {
×
915
                "type": "geometry",
916
                "value": self.output_column.value,
917
            }
918
        elif isinstance(self.output_column, str):
×
919
            output_column_dict = {
×
920
                "type": "column",
921
                "value": self.output_column,
922
            }
923
        else:
924
            raise NotImplementedError("Invalid output column type")
×
925

926
        params = {
×
927
            "expression": self.expression,
928
            "inputColumns": self.input_columns,
929
            "outputColumn": output_column_dict,
930
        }  # type: Dict[str, Any]
931

932
        if self.geometry_column_name:
×
933
            params["geometryColumnName"] = self.geometry_column_name
×
934

935
        if self.output_measurement:
×
936
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
×
937

938
        return {
×
939
            "type": self.name(),
940
            "params": params,
941
            "sources": {
942
                "vector": self.source.to_dict()
943
            }
944
        }
945

946
    @classmethod
1✔
947
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> VectorExpression:
1✔
948
        if operator_dict["type"] != "Expression":
×
949
            raise ValueError("Invalid operator type")
×
950

951
        geometry_column_name = None
×
952
        if "geometryColumnName" in operator_dict["params"]:
×
953
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
×
954

955
        output_measurement = None
×
956
        if "outputMeasurement" in operator_dict["params"]:
×
957
            output_measurement = Measurement.from_response(operator_dict["params"]["outputMeasurement"])
×
958

959
        return VectorExpression(
×
960
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
961
            expression=operator_dict["params"]["expression"],
962
            input_columns=operator_dict["params"]["inputColumns"],
963
            output_column=operator_dict["params"]["outputColumn"],
964
            geometry_column_name=geometry_column_name,
965
            output_measurement=output_measurement,
966
        )
967

968

969
class TemporalRasterAggregation(RasterOperator):
1✔
970
    '''A TemporalRasterAggregation operator.'''
971
    # pylint: disable=too-many-instance-attributes
972

973
    source: RasterOperator
1✔
974
    aggregation_type: Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"]
1✔
975
    ignore_no_data: bool = False
1✔
976
    window_granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days"
1✔
977
    window_size: int = 1
1✔
978
    output_type: Optional[Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]] = None
1✔
979
    percentile: Optional[float] = None
1✔
980
    window_ref: Optional[np.datetime64] = None
1✔
981

982
    # pylint: disable=too-many-arguments,too-many-positional-arguments
983
    def __init__(self,
1✔
984
                 source: RasterOperator,
985
                 aggregation_type:
986
                 Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"],
987
                 ignore_no_data: bool = False,
988
                 granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days",
989
                 window_size: int = 1,
990
                 output_type:
991
                 Optional[Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]] = None,
992
                 percentile: Optional[float] = None,
993
                 window_reference: Optional[Union[datetime.datetime, np.datetime64]] = None
994
                 ):
995
        '''Creates a new TemporalRasterAggregation operator.'''
996
        self.source = source
1✔
997
        self.aggregation_type = aggregation_type
1✔
998
        self.ignore_no_data = ignore_no_data
1✔
999
        self.window_granularity = granularity
1✔
1000
        self.window_size = window_size
1✔
1001
        self.output_type = output_type
1✔
1002
        if self.aggregation_type == "percentileEstimate":
1✔
1003
            if percentile is None:
×
1004
                raise ValueError("Percentile must be set for percentileEstimate")
×
1005
            if percentile <= 0.0 or percentile > 1.0:
×
1006
                raise ValueError("Percentile must be > 0.0 and <= 1.0")
×
1007
            self.percentile = percentile
×
1008
        if window_reference is not None:
1✔
1009
            if isinstance(window_reference, np.datetime64):
×
1010
                self.window_ref = window_reference
×
1011
            elif isinstance(window_reference, datetime.datetime):
×
1012
                # We assume that a datetime without a timezone means UTC
1013
                if window_reference.tzinfo is not None:
×
1014
                    window_reference = window_reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
×
1015
                self.window_ref = np.datetime64(window_reference)
×
1016
            else:
1017
                raise ValueError("`window_reference` must be of type `datetime.datetime` or `numpy.datetime64`")
×
1018

1019
    def name(self) -> str:
1✔
1020
        return 'TemporalRasterAggregation'
1✔
1021

1022
    def to_dict(self) -> Dict[str, Any]:
1✔
1023
        w_ref = self.window_ref.astype('datetime64[ms]').astype(int) if self.window_ref is not None else None
1✔
1024

1025
        return {
1✔
1026
            "type": self.name(),
1027
            "params": {
1028
                "aggregation": {
1029
                    "type": self.aggregation_type,
1030
                    "ignoreNoData": self.ignore_no_data,
1031
                    "percentile": self.percentile
1032
                },
1033
                "window": {
1034
                    "granularity": self.window_granularity,
1035
                    "step": self.window_size
1036
                },
1037
                "windowReference": w_ref,
1038
                "outputType": self.output_type
1039
            },
1040
            "sources": {
1041
                "raster": self.source.to_dict()
1042
            }
1043
        }
1044

1045
    @classmethod
1✔
1046
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'TemporalRasterAggregation':
1✔
1047
        if operator_dict["type"] != "TemporalRasterAggregation":
1✔
1048
            raise ValueError("Invalid operator type")
×
1049

1050
        w_ref: Optional[Union[datetime.datetime, np.datetime64]] = None
1✔
1051
        if "windowReference" in operator_dict["params"]:
1✔
1052
            t_ref = operator_dict["params"]["windowReference"]
1✔
1053
            if isinstance(t_ref, str):
1✔
1054
                w_ref = datetime.datetime.fromisoformat(t_ref)
×
1055
            if isinstance(t_ref, int):
1✔
1056
                w_ref = np.datetime64(t_ref, 'ms')
×
1057

1058
        percentile = None
1✔
1059
        if "percentile" in operator_dict["params"]["aggregation"]:
1✔
1060
            percentile = operator_dict["params"]["aggregation"]["percentile"]
1✔
1061

1062
        return TemporalRasterAggregation(
1✔
1063
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
1064
            aggregation_type=operator_dict["params"]["aggregation"]["type"],
1065
            ignore_no_data=operator_dict["params"]["aggregation"]["ignoreNoData"],
1066
            granularity=operator_dict["params"]["window"]["granularity"],
1067
            window_size=operator_dict["params"]["window"]["step"],
1068
            output_type=operator_dict["params"]["outputType"],
1069
            window_reference=w_ref,
1070
            percentile=percentile
1071
        )
1072

1073

1074
class TimeShift(Operator):
1✔
1075
    '''A RasterTypeConversion operator.'''
1076

1077
    source: Union[RasterOperator, VectorOperator]
1✔
1078
    shift_type: Literal["relative", "absolute"]
1✔
1079
    granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"]
1✔
1080
    value: int
1✔
1081

1082
    def __init__(self,
1✔
1083
                 source: Union[RasterOperator, VectorOperator],
1084
                 shift_type: Literal["relative", "absolute"],
1085
                 granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"],
1086
                 value: int,
1087
                 ):
1088
        '''Creates a new RasterTypeConversion operator.'''
1089
        if shift_type == 'absolute':
1✔
1090
            raise NotImplementedError("Absolute time shifts are not supported yet")
×
1091
        self.source = source
1✔
1092
        self.shift_type = shift_type
1✔
1093
        self.granularity = granularity
1✔
1094
        self.value = value
1✔
1095

1096
    def name(self) -> str:
1✔
1097
        return 'TimeShift'
1✔
1098

1099
    def data_type(self) -> Literal['Vector', 'Raster']:
1✔
1100
        return self.source.data_type()
×
1101

1102
    def as_vector(self) -> VectorOperator:
1✔
1103
        '''Casts this operator to a VectorOperator.'''
1104
        if self.data_type() != 'Vector':
×
1105
            raise TypeError("Cannot cast to VectorOperator")
×
1106
        return cast(VectorOperator, self)
×
1107

1108
    def as_raster(self) -> RasterOperator:
1✔
1109
        '''Casts this operator to a RasterOperator.'''
1110
        if self.data_type() != 'Raster':
×
1111
            raise TypeError("Cannot cast to RasterOperator")
×
1112
        return cast(RasterOperator, self)
×
1113

1114
    def to_dict(self) -> Dict[str, Any]:
1✔
1115
        return {
1✔
1116
            "type": self.name(),
1117
            "params": {
1118
                "type": self.shift_type,
1119
                "granularity": self.granularity,
1120
                "value": self.value
1121
            },
1122
            "sources": {
1123
                "source": self.source.to_dict()
1124
            }
1125
        }
1126

1127
    @classmethod
1✔
1128
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'TimeShift':
1✔
1129
        '''Constructs the operator from the given dictionary.'''
1130
        if operator_dict["type"] != "TimeShift":
×
1131
            raise ValueError("Invalid operator type")
×
1132
        source: Union[RasterOperator, VectorOperator]
1133
        try:
×
1134
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1135
        except ValueError:
×
1136
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1137

1138
        return TimeShift(
×
1139
            source=source,
1140
            shift_type=operator_dict["params"]["type"],
1141
            granularity=operator_dict["params"]["granularity"],
1142
            value=operator_dict["params"]["value"]
1143
        )
1144

1145

1146
class RenameBands:
1✔
1147
    '''Base class for renaming bands of a raster.'''
1148

1149
    @abstractmethod
1✔
1150
    def to_dict(self) -> Dict[str, Any]:
1✔
1151
        pass
×
1152

1153
    @classmethod
1✔
1154
    def from_dict(cls, rename_dict: Dict[str, Any]) -> 'RenameBands':
1✔
1155
        '''Returns a RenameBands object from a dictionary.'''
1156
        if rename_dict["type"] == "default":
×
1157
            return RenameBandsDefault()
×
1158
        if rename_dict["type"] == "suffix":
×
1159
            return RenameBandsSuffix(cast(List[str], rename_dict["values"]))
×
1160
        if rename_dict["type"] == "rename":
×
1161
            return RenameBandsRename(cast(List[str], rename_dict["values"]))
×
1162
        raise ValueError("Invalid rename type")
×
1163

1164
    @classmethod
1✔
1165
    def default(cls) -> 'RenameBands':
1✔
1166
        return RenameBandsDefault()
×
1167

1168
    @classmethod
1✔
1169
    def suffix(cls, values: List[str]) -> 'RenameBands':
1✔
1170
        return RenameBandsSuffix(values)
×
1171

1172
    @classmethod
1✔
1173
    def rename(cls, values: List[str]) -> 'RenameBands':
1✔
1174
        return RenameBandsRename(values)
×
1175

1176

1177
class RenameBandsDefault(RenameBands):
1✔
1178
    '''Rename bands with default suffix.'''
1179

1180
    def to_dict(self) -> Dict[str, Any]:
1✔
1181
        return {
1✔
1182
            "type": "default"
1183
        }
1184

1185

1186
class RenameBandsSuffix(RenameBands):
1✔
1187
    '''Rename bands with custom suffixes.'''
1188

1189
    suffixes: List[str]
1✔
1190

1191
    def __init__(self, suffixes: List[str]) -> None:
1✔
1192
        self.suffixes = suffixes
×
1193
        super().__init__()
×
1194

1195
    def to_dict(self) -> Dict[str, Any]:
1✔
1196
        return {
×
1197
            "type": "suffix",
1198
            "values": self.suffixes
1199
        }
1200

1201

1202
class RenameBandsRename(RenameBands):
1✔
1203
    '''Rename bands with new names.'''
1204

1205
    new_names: List[str]
1✔
1206

1207
    def __init__(self, new_names: List[str]) -> None:
1✔
1208
        self.new_names = new_names
×
1209
        super().__init__()
×
1210

1211
    def to_dict(self) -> Dict[str, Any]:
1✔
1212
        return {
×
1213
            "type": "rename",
1214
            "values": self.new_names
1215
        }
1216

1217

1218
class RasterStacker(RasterOperator):
1✔
1219
    '''The RasterStacker operator.'''
1220

1221
    sources: List[RasterOperator]
1✔
1222
    rename: RenameBands
1✔
1223

1224
    # pylint: disable=too-many-arguments
1225
    def __init__(self,
1✔
1226
                 sources: List[RasterOperator],
1227
                 rename: RenameBands = RenameBandsDefault()
1228
                 ):
1229
        '''Creates a new RasterStacker operator.'''
1230
        self.sources = sources
1✔
1231
        self.rename = rename
1✔
1232

1233
    def name(self) -> str:
1✔
1234
        return 'RasterStacker'
1✔
1235

1236
    def to_dict(self) -> Dict[str, Any]:
1✔
1237
        return {
1✔
1238
            "type": self.name(),
1239
            "params": {
1240
                "renameBands": self.rename.to_dict()
1241
            },
1242
            "sources": {
1243
                "rasters": [raster_source.to_dict() for raster_source in self.sources]
1244
            }
1245
        }
1246

1247
    @classmethod
1✔
1248
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterStacker':
1✔
1249
        if operator_dict["type"] != "RasterStacker":
×
1250
            raise ValueError("Invalid operator type")
×
1251

1252
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
×
1253
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
×
1254

1255
        return RasterStacker(
×
1256
            sources=sources,
1257
            rename=rename
1258
        )
1259

1260

1261
class BandNeighborhoodAggregate(RasterOperator):
1✔
1262
    '''The BandNeighborhoodAggregate operator.'''
1263

1264
    source: RasterOperator
1✔
1265
    aggregate: BandNeighborhoodAggregateParams
1✔
1266

1267
    # pylint: disable=too-many-arguments
1268
    def __init__(self,
1✔
1269
                 source: RasterOperator,
1270
                 aggregate: BandNeighborhoodAggregateParams
1271
                 ):
1272
        '''Creates a new BandNeighborhoodAggregate operator.'''
1273
        self.source = source
×
1274
        self.aggregate = aggregate
×
1275

1276
    def name(self) -> str:
1✔
1277
        return 'BandNeighborhoodAggregate'
×
1278

1279
    def to_dict(self) -> Dict[str, Any]:
1✔
1280
        return {
×
1281
            "type": self.name(),
1282
            "params": {
1283
                "aggregate": self.aggregate.to_dict()
1284
            },
1285
            "sources": {
1286
                "raster": self.source.to_dict()
1287
            }
1288
        }
1289

1290
    @classmethod
1✔
1291
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregate':
1✔
1292
        if operator_dict["type"] != "BandNeighborhoodAggregate":
×
1293
            raise ValueError("Invalid operator type")
×
1294

1295
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1296
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1297

1298
        return BandNeighborhoodAggregate(
×
1299
            source=source,
1300
            aggregate=aggregate
1301
        )
1302

1303

1304
class BandNeighborhoodAggregateParams:
1✔
1305
    '''Abstract base class for band neighborhood aggregate params.'''
1306

1307
    @abstractmethod
1✔
1308
    def to_dict(self) -> Dict[str, Any]:
1✔
1309
        pass
×
1310

1311
    @classmethod
1✔
1312
    def from_dict(cls, band_neighborhood_aggregate_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregateParams':
1✔
1313
        '''Returns a BandNeighborhoodAggregate object from a dictionary.'''
1314
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
1315
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
1316
        if band_neighborhood_aggregate_dict["type"] == "average":
×
1317
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
1318
        raise ValueError("Invalid neighborhood aggregate type")
×
1319

1320
    @classmethod
1✔
1321
    def first_derivative(cls, equally_spaced_band_distance: float) -> 'BandNeighborhoodAggregateParams':
1✔
1322
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1323

1324
    @classmethod
1✔
1325
    def average(cls, window_size: int) -> 'BandNeighborhoodAggregateParams':
1✔
1326
        return BandNeighborhoodAggregateAverage(window_size)
×
1327

1328

1329
@dataclass
1✔
1330
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1✔
1331
    '''The first derivative band neighborhood aggregate.'''
1332

1333
    equally_spaced_band_distance: float
1✔
1334

1335
    @classmethod
1✔
1336
    def from_dict(cls, band_neighborhood_aggregate_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregateParams':
1✔
1337
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
1338
            raise ValueError("Invalid neighborhood aggregate type")
×
1339

1340
        return BandNeighborhoodAggregateFirstDerivative(
×
1341
            band_neighborhood_aggregate_dict["bandDistance"]["distance"]
1342
        )
1343

1344
    def to_dict(self) -> Dict[str, Any]:
1✔
1345
        return {
×
1346
            "type": "firstDerivative",
1347
            "bandDistance": {
1348
                "type": "equallySpaced",
1349
                "distance": self.equally_spaced_band_distance
1350
            }
1351
        }
1352

1353

1354
@dataclass
1✔
1355
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1✔
1356
    '''The average band neighborhood aggregate.'''
1357

1358
    window_size: int
1✔
1359

1360
    def to_dict(self) -> Dict[str, Any]:
1✔
1361
        return {
×
1362
            "type": "average",
1363
            "windowSize": self.window_size
1364
        }
1365

1366

1367
class Onnx(RasterOperator):
1✔
1368
    '''Onnx ML operator.'''
1369

1370
    source: RasterOperator
1✔
1371
    model: str
1✔
1372

1373
    # pylint: disable=too-many-arguments
1374
    def __init__(self,
1✔
1375
                 source: RasterOperator,
1376
                 model: str
1377
                 ):
1378
        '''Creates a new Onnx operator.'''
1379
        self.source = source
×
1380
        self.model = model
×
1381

1382
    def name(self) -> str:
1✔
1383
        return 'Onnx'
×
1384

1385
    def to_dict(self) -> Dict[str, Any]:
1✔
1386
        return {
×
1387
            "type": self.name(),
1388
            "params": {
1389
                "model": self.model
1390
            },
1391
            "sources": {
1392
                "raster": self.source.to_dict()
1393
            }
1394
        }
1395

1396
    @classmethod
1✔
1397
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'Onnx':
1✔
1398
        if operator_dict["type"] != "Onnx":
×
1399
            raise ValueError("Invalid operator type")
×
1400

1401
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1402
        model = operator_dict["params"]["model"]
×
1403

1404
        return Onnx(
×
1405
            source=source,
1406
            model=model
1407
        )
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