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

geo-engine / geoengine-python / 15077071955

16 May 2025 08:42PM UTC coverage: 76.806% (+0.1%) from 76.662%
15077071955

Pull #205

github

web-flow
Merge 6d7fc1825 into ad36bedb8
Pull Request #205: Add-ml-model-shape

2785 of 3626 relevant lines covered (76.81%)

0.77 hits per line

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

67.92
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
    input_resolution: Optional[Tuple[float, float]] = None
1✔
201

202
    def __init__(
1✔
203
        self,
204
            source_operator: RasterOperator,
205
            interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear",
206
            input_resolution: Optional[Tuple[float, float]] = None
207
    ):
208
        '''Creates a new interpolation operator.'''
209
        self.source = source_operator
1✔
210
        self.interpolation = interpolation
1✔
211
        self.input_resolution = input_resolution
1✔
212

213
    def name(self) -> str:
1✔
214
        return 'Interpolation'
1✔
215

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

218
        input_res: Dict[str, Union[str, float]]
219
        if self.input_resolution is None:
1✔
220
            input_res = {
1✔
221
                "type": "source"
222
            }
223
        else:
224
            input_res = {
×
225
                "type": "value",
226
                "x": self.input_resolution[0],
227
                "y": self.input_resolution[1]
228
            }
229

230
        return {
1✔
231
            "type": self.name(),
232
            "params": {
233
                "interpolation": self.interpolation,
234
                "inputResolution": input_res
235
            },
236
            "sources": {
237
                "raster": self.source.to_dict()
238
            }
239
        }
240

241
    @classmethod
1✔
242
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> Interpolation:
1✔
243
        '''Returns an operator from a dictionary.'''
244
        if operator_dict["type"] != "Interpolation":
1✔
245
            raise ValueError("Invalid operator type")
×
246

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

249
        def parse_input_params(params: Dict[str, Any]) -> Optional[Tuple[float, float]]:
1✔
250
            if 'type' not in params:
1✔
251
                return None
×
252
            if params['type'] == 'source':
1✔
253
                return None
1✔
254
            if params['type'] == 'value':
×
255
                return (float(params['x']), float(params['y']))
×
256
            raise ValueError(f"Invalid input resolution type {params['type']}")
×
257

258
        input_resolution = parse_input_params(cast(Dict[str, Any], operator_dict['params']['inputResolution']))
1✔
259

260
        return Interpolation(
1✔
261
            source_operator=source,
262
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict['params']['interpolation']),
263
            input_resolution=input_resolution
264
        )
265

266

267
class ColumnNames:
1✔
268
    '''Base class for deriving column names from bands of a raster.'''
269

270
    @abstractmethod
1✔
271
    def to_dict(self) -> Dict[str, Any]:
1✔
272
        pass
×
273

274
    @classmethod
1✔
275
    def from_dict(cls, rename_dict: Dict[str, Any]) -> 'ColumnNames':
1✔
276
        '''Returns a ColumnNames object from a dictionary.'''
277
        if rename_dict["type"] == "default":
1✔
278
            return ColumnNamesDefault()
×
279
        if rename_dict["type"] == "suffix":
1✔
280
            return ColumnNamesSuffix(cast(List[str], rename_dict["values"]))
×
281
        if rename_dict["type"] == "names":
1✔
282
            return ColumnNamesNames(cast(List[str], rename_dict["values"]))
1✔
283
        raise ValueError("Invalid rename type")
×
284

285
    @classmethod
1✔
286
    def default(cls) -> 'ColumnNames':
1✔
287
        return ColumnNamesDefault()
×
288

289
    @classmethod
1✔
290
    def suffix(cls, values: List[str]) -> 'ColumnNames':
1✔
291
        return ColumnNamesSuffix(values)
×
292

293
    @classmethod
1✔
294
    def rename(cls, values: List[str]) -> 'ColumnNames':
1✔
295
        return ColumnNamesNames(values)
×
296

297

298
class ColumnNamesDefault(ColumnNames):
1✔
299
    '''column names with default suffix.'''
300

301
    def to_dict(self) -> Dict[str, Any]:
1✔
302
        return {
×
303
            "type": "default"
304
        }
305

306

307
class ColumnNamesSuffix(ColumnNames):
1✔
308
    '''Rename bands with custom suffixes.'''
309

310
    suffixes: List[str]
1✔
311

312
    def __init__(self, suffixes: List[str]) -> None:
1✔
313
        self.suffixes = suffixes
×
314
        super().__init__()
×
315

316
    def to_dict(self) -> Dict[str, Any]:
1✔
317
        return {
×
318
            "type": "suffix",
319
            "values": self.suffixes
320
        }
321

322

323
class ColumnNamesNames(ColumnNames):
1✔
324
    '''Rename bands with new names.'''
325

326
    new_names: List[str]
1✔
327

328
    def __init__(self, new_names: List[str]) -> None:
1✔
329
        self.new_names = new_names
1✔
330
        super().__init__()
1✔
331

332
    def to_dict(self) -> Dict[str, Any]:
1✔
333
        return {
1✔
334
            "type": "names",
335
            "values": self.new_names
336
        }
337

338

339
class RasterVectorJoin(VectorOperator):
1✔
340
    '''A RasterVectorJoin operator.'''
341
    raster_sources: List[RasterOperator]
1✔
342
    vector_source: VectorOperator
1✔
343
    names: ColumnNames
1✔
344
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1✔
345
    temporal_aggregation_ignore_nodata: bool = False
1✔
346
    feature_aggregation: Literal["first", "mean"] = "mean"
1✔
347
    feature_aggregation_ignore_nodata: bool = False
1✔
348

349
    # pylint: disable=too-many-arguments,too-many-positional-arguments
350
    def __init__(self,
1✔
351
                 raster_sources: List[RasterOperator],
352
                 vector_source: VectorOperator,
353
                 names: ColumnNames,
354
                 temporal_aggregation: Literal["none", "first", "mean"] = "none",
355
                 temporal_aggregation_ignore_nodata: bool = False,
356
                 feature_aggregation: Literal["first", "mean"] = "mean",
357
                 feature_aggregation_ignore_nodata: bool = False,
358
                 ):
359
        '''Creates a new RasterVectorJoin operator.'''
360
        self.raster_source = raster_sources
1✔
361
        self.vector_source = vector_source
1✔
362
        self.names = names
1✔
363
        self.temporal_aggregation = temporal_aggregation
1✔
364
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1✔
365
        self.feature_aggregation = feature_aggregation
1✔
366
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1✔
367

368
    def name(self) -> str:
1✔
369
        return 'RasterVectorJoin'
1✔
370

371
    def to_dict(self) -> Dict[str, Any]:
1✔
372
        return {
1✔
373
            "type": self.name(),
374
            "params": {
375
                "names": self.names.to_dict(),
376
                "temporalAggregation": self.temporal_aggregation,
377
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
378
                "featureAggregation": self.feature_aggregation,
379
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
380
            },
381
            "sources": {
382
                "vector": self.vector_source.to_dict(),
383
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source]
384
            }
385
        }
386

387
    @classmethod
1✔
388
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterVectorJoin':
1✔
389
        '''Returns an operator from a dictionary.'''
390
        if operator_dict["type"] != "RasterVectorJoin":
1✔
391
            raise ValueError("Invalid operator type")
×
392

393
        vector_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['vector']))
1✔
394
        raster_sources = [
1✔
395
            RasterOperator.from_operator_dict(raster_source) for raster_source in cast(
396
                List[Dict[str, Any]], operator_dict['sources']['rasters']
397
            )
398
        ]
399

400
        params = operator_dict['params']
1✔
401
        return RasterVectorJoin(
1✔
402
            raster_sources=raster_sources,
403
            vector_source=vector_source,
404
            names=ColumnNames.from_dict(params['names']),
405
            temporal_aggregation=cast(Literal["none", "first", "mean"], params['temporalAggregation']),
406
            temporal_aggregation_ignore_nodata=cast(bool, params['temporalAggregationIgnoreNoData']),
407
            feature_aggregation=cast(Literal["first", "mean"], params['featureAggregation']),
408
            feature_aggregation_ignore_nodata=cast(bool, params['featureAggregationIgnoreNoData']),
409
        )
410

411

412
class PointInPolygonFilter(VectorOperator):
1✔
413
    '''A PointInPolygonFilter operator.'''
414

415
    point_source: VectorOperator
1✔
416
    polygon_source: VectorOperator
1✔
417

418
    def __init__(self,
1✔
419
                 point_source: VectorOperator,
420
                 polygon_source: VectorOperator,
421
                 ):
422
        '''Creates a new PointInPolygonFilter filter operator.'''
423
        self.point_source = point_source
1✔
424
        self.polygon_source = polygon_source
1✔
425

426
    def name(self) -> str:
1✔
427
        return 'PointInPolygonFilter'
1✔
428

429
    def to_dict(self) -> Dict[str, Any]:
1✔
430
        return {
1✔
431
            "type": self.name(),
432
            "params": {},
433
            "sources": {
434
                "points": self.point_source.to_dict(),
435
                "polygons": self.polygon_source.to_dict()
436
            }
437
        }
438

439
    @classmethod
1✔
440
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> PointInPolygonFilter:
1✔
441
        '''Returns an operator from a dictionary.'''
442
        if operator_dict["type"] != "PointInPolygonFilter":
1✔
443
            raise ValueError("Invalid operator type")
×
444

445
        point_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['points']))
1✔
446
        polygon_source = VectorOperator.from_operator_dict(cast(Dict[str, Any], operator_dict['sources']['polygons']))
1✔
447

448
        return PointInPolygonFilter(
1✔
449
            point_source=point_source,
450
            polygon_source=polygon_source,
451
        )
452

453

454
class RasterScaling(RasterOperator):
1✔
455
    '''A RasterScaling operator.
456

457
    This operator scales the values of a raster by a given slope and offset.
458

459
    The scaling is done as follows:
460
    y = (x - offset) / slope
461

462
    The unscale mode is the inverse of the scale mode:
463
    x = y * slope + offset
464

465
    '''
466

467
    source: RasterOperator
1✔
468
    slope: Optional[Union[float, str]] = None
1✔
469
    offset: Optional[Union[float, str]] = None
1✔
470
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1✔
471
    output_measurement: Optional[str] = None
1✔
472

473
    def __init__(self,
1✔
474
                 # pylint: disable=too-many-arguments,too-many-positional-arguments
475
                 source: RasterOperator,
476
                 slope: Optional[Union[float, str]] = None,
477
                 offset: Optional[Union[float, str]] = None,
478
                 scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
479
                 output_measurement: Optional[str] = None
480
                 ):
481
        '''Creates a new RasterScaling operator.'''
482
        self.source = source
1✔
483
        self.slope = slope
1✔
484
        self.offset = offset
1✔
485
        self.scaling_mode = scaling_mode
1✔
486
        self.output_measurement = output_measurement
1✔
487
        if output_measurement is not None:
1✔
488
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
489

490
    def name(self) -> str:
1✔
491
        return 'RasterScaling'
1✔
492

493
    def to_dict(self) -> Dict[str, Any]:
1✔
494
        def offset_scale_dict(key_or_value: Optional[Union[float, str]]) -> Dict[str, Any]:
1✔
495
            if key_or_value is None:
1✔
496
                return {"type": "auto"}
1✔
497

498
            if isinstance(key_or_value, float):
1✔
499
                return {"type": "constant", "value": key_or_value}
1✔
500

501
            if isinstance(key_or_value, int):
×
502
                return {"type": "constant", "value": float(key_or_value)}
×
503

504
            # TODO: incorporate `domain` field
505
            return {"type": "metadataKey", "key": key_or_value}
×
506

507
        return {
1✔
508
            "type": self.name(),
509
            "params": {
510
                "offset": offset_scale_dict(self.offset),
511
                "slope": offset_scale_dict(self.slope),
512
                "scalingMode": self.scaling_mode
513
            },
514
            "sources": {
515
                "raster": self.source.to_dict()
516
            }
517
        }
518

519
    @classmethod
1✔
520
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterScaling':
1✔
521
        if operator_dict["type"] != "RasterScaling":
1✔
522
            raise ValueError("Invalid operator type")
×
523

524
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
525
        params = operator_dict["params"]
1✔
526

527
        def offset_slope_reverse(key_or_value: Optional[Dict[str, Any]]) -> Optional[Union[float, str]]:
1✔
528
            if key_or_value is None:
1✔
529
                return None
×
530
            if key_or_value["type"] == "constant":
1✔
531
                return key_or_value["value"]
1✔
532
            if key_or_value["type"] == "metadataKey":
1✔
533
                return key_or_value["key"]
×
534
            return None
1✔
535

536
        return RasterScaling(
1✔
537
            source_operator,
538
            slope=offset_slope_reverse(params["slope"]),
539
            offset=offset_slope_reverse(params["offset"]),
540
            scaling_mode=params["scalingMode"],
541
            output_measurement=params.get("outputMeasurement", None)
542
        )
543

544

545
class RasterTypeConversion(RasterOperator):
1✔
546
    '''A RasterTypeConversion operator.'''
547

548
    source: RasterOperator
1✔
549
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
550

551
    def __init__(self,
1✔
552
                 source: RasterOperator,
553
                 output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
554
                 ):
555
        '''Creates a new RasterTypeConversion operator.'''
556
        self.source = source
1✔
557
        self.output_data_type = output_data_type
1✔
558

559
    def name(self) -> str:
1✔
560
        return 'RasterTypeConversion'
1✔
561

562
    def to_dict(self) -> Dict[str, Any]:
1✔
563
        return {
1✔
564
            "type": self.name(),
565
            "params": {
566
                "outputDataType": self.output_data_type
567
            },
568
            "sources": {
569
                "raster": self.source.to_dict()
570
            }
571
        }
572

573
    @classmethod
1✔
574
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterTypeConversion':
1✔
575
        if operator_dict["type"] != "RasterTypeConversion":
1✔
576
            raise ValueError("Invalid operator type")
×
577

578
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
579

580
        return RasterTypeConversion(
1✔
581
            source_operator,
582
            output_data_type=operator_dict["params"]["outputDataType"]
583
        )
584

585

586
class Reprojection(Operator):
1✔
587
    '''A Reprojection operator.'''
588
    source: Operator
1✔
589
    target_spatial_reference: str
1✔
590

591
    def __init__(self,
1✔
592
                 source: Operator,
593
                 target_spatial_reference: str
594
                 ):
595
        '''Creates a new Reprojection operator.'''
596
        self.source = source
1✔
597
        self.target_spatial_reference = target_spatial_reference
1✔
598

599
    def data_type(self) -> Literal['Raster', 'Vector']:
1✔
600
        return self.source.data_type()
×
601

602
    def name(self) -> str:
1✔
603
        return 'Reprojection'
1✔
604

605
    def to_dict(self) -> Dict[str, Any]:
1✔
606
        return {
1✔
607
            "type": self.name(),
608
            "params": {
609
                "targetSpatialReference": self.target_spatial_reference
610
            },
611
            "sources": {
612
                "source": self.source.to_dict()
613
            }
614
        }
615

616
    def as_vector(self) -> VectorOperator:
1✔
617
        '''Casts this operator to a VectorOperator.'''
618
        if self.data_type() != 'Vector':
×
619
            raise TypeError("Cannot cast to VectorOperator")
×
620
        return cast(VectorOperator, self)
×
621

622
    def as_raster(self) -> RasterOperator:
1✔
623
        '''Casts this operator to a RasterOperator.'''
624
        if self.data_type() != 'Raster':
×
625
            raise TypeError("Cannot cast to RasterOperator")
×
626
        return cast(RasterOperator, self)
×
627

628
    @classmethod
1✔
629
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'Reprojection':
1✔
630
        '''Constructs the operator from the given dictionary.'''
631
        if operator_dict["type"] != "Reprojection":
1✔
632
            raise ValueError("Invalid operator type")
×
633

634
        source_operator: Union[RasterOperator, VectorOperator]
635
        try:
1✔
636
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1✔
637
        except ValueError:
×
638
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
639

640
        return Reprojection(
1✔
641
            source=cast(Operator, source_operator),
642
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"]
643
        )
644

645

646
class Expression(RasterOperator):
1✔
647
    '''An Expression operator.'''
648

649
    expression: str
1✔
650
    source: RasterOperator
1✔
651
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
652
    map_no_data: bool = False
1✔
653
    output_band: Optional[RasterBandDescriptor] = None
1✔
654

655
    # pylint: disable=too-many-arguments,too-many-positional-arguments
656
    def __init__(self,
1✔
657
                 expression: str,
658
                 source: RasterOperator,
659
                 output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
660
                 map_no_data: bool = False,
661
                 output_band: Optional[RasterBandDescriptor] = None,
662
                 ):
663
        '''Creates a new Expression operator.'''
664
        self.expression = expression
1✔
665
        self.source = source
1✔
666
        self.output_type = output_type
1✔
667
        self.map_no_data = map_no_data
1✔
668
        self.output_band = output_band
1✔
669

670
    def name(self) -> str:
1✔
671
        return 'Expression'
1✔
672

673
    def to_dict(self) -> Dict[str, Any]:
1✔
674
        params = {
1✔
675
            "expression": self.expression,
676
            "outputType": self.output_type,
677
            "mapNoData": self.map_no_data,
678
        }
679
        if self.output_band is not None:
1✔
680
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1✔
681

682
        return {
1✔
683
            "type": self.name(),
684
            "params": params,
685
            "sources": {
686
                "raster": self.source.to_dict()
687
            }
688
        }
689

690
    @classmethod
1✔
691
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'Expression':
1✔
692
        if operator_dict["type"] != "Expression":
1✔
693
            raise ValueError("Invalid operator type")
×
694

695
        output_band = None
1✔
696
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1✔
697
            raster_band_descriptor = geoengine_openapi_client.RasterBandDescriptor.from_dict(
1✔
698
                operator_dict["params"]["outputBand"]
699
            )
700
            if raster_band_descriptor is None:
1✔
701
                raise ValueError("Invalid output band")
×
702
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1✔
703

704
        return Expression(
1✔
705
            expression=operator_dict["params"]["expression"],
706
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
707
            output_type=operator_dict["params"]["outputType"],
708
            map_no_data=operator_dict["params"]["mapNoData"],
709
            output_band=output_band
710
        )
711

712

713
class BandwiseExpression(RasterOperator):
1✔
714
    '''A bandwise Expression operator.'''
715

716
    expression: str
1✔
717
    source: RasterOperator
1✔
718
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
719
    map_no_data: bool = False
1✔
720

721
    # pylint: disable=too-many-arguments
722
    def __init__(self,
1✔
723
                 expression: str,
724
                 source: RasterOperator,
725
                 output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
726
                 map_no_data: bool = False,
727
                 ):
728
        '''Creates a new Expression operator.'''
729
        self.expression = expression
×
730
        self.source = source
×
731
        self.output_type = output_type
×
732
        self.map_no_data = map_no_data
×
733

734
    def name(self) -> str:
1✔
735
        return 'BandwiseExpression'
×
736

737
    def to_dict(self) -> Dict[str, Any]:
1✔
738
        params = {
×
739
            "expression": self.expression,
740
            "outputType": self.output_type,
741
            "mapNoData": self.map_no_data,
742
        }
743

744
        return {
×
745
            "type": self.name(),
746
            "params": params,
747
            "sources": {
748
                "raster": self.source.to_dict()
749
            }
750
        }
751

752
    @classmethod
1✔
753
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'BandwiseExpression':
1✔
754
        if operator_dict["type"] != "BandwiseExpression":
×
755
            raise ValueError("Invalid operator type")
×
756

757
        return BandwiseExpression(
×
758
            expression=operator_dict["params"]["expression"],
759
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
760
            output_type=operator_dict["params"]["outputType"],
761
            map_no_data=operator_dict["params"]["mapNoData"],
762
        )
763

764

765
class GeoVectorDataType(Enum):
1✔
766
    '''The output type of geometry vector data.'''
767
    MULTI_POINT = "MultiPoint"
1✔
768
    MULTI_LINE_STRING = "MultiLineString"
1✔
769
    MULTI_POLYGON = "MultiPolygon"
1✔
770

771

772
class VectorExpression(VectorOperator):
1✔
773
    '''The `VectorExpression` operator.'''
774

775
    source: VectorOperator
1✔
776

777
    expression: str
1✔
778
    input_columns: List[str]
1✔
779
    output_column: str | GeoVectorDataType
1✔
780
    geometry_column_name = None
1✔
781
    output_measurement: Optional[Measurement] = None
1✔
782

783
    # pylint: disable=too-many-arguments
784
    def __init__(self,
1✔
785
                 source: VectorOperator,
786
                 *,
787
                 expression: str,
788
                 input_columns: List[str],
789
                 output_column: str | GeoVectorDataType,
790
                 geometry_column_name: Optional[str] = None,
791
                 output_measurement: Optional[Measurement] = None,
792
                 ):
793
        '''Creates a new VectorExpression operator.'''
794
        self.source = source
×
795

796
        self.expression = expression
×
797
        self.input_columns = input_columns
×
798
        self.output_column = output_column
×
799

800
        self.geometry_column_name = geometry_column_name
×
801
        self.output_measurement = output_measurement
×
802

803
    def name(self) -> str:
1✔
804
        return 'VectorExpression'
×
805

806
    def to_dict(self) -> Dict[str, Any]:
1✔
807
        output_column_dict = None
×
808
        if isinstance(self.output_column, GeoVectorDataType):
×
809
            output_column_dict = {
×
810
                "type": "geometry",
811
                "value": self.output_column.value,
812
            }
813
        elif isinstance(self.output_column, str):
×
814
            output_column_dict = {
×
815
                "type": "column",
816
                "value": self.output_column,
817
            }
818
        else:
819
            raise NotImplementedError("Invalid output column type")
×
820

821
        params = {
×
822
            "expression": self.expression,
823
            "inputColumns": self.input_columns,
824
            "outputColumn": output_column_dict,
825
        }  # type: Dict[str, Any]
826

827
        if self.geometry_column_name:
×
828
            params["geometryColumnName"] = self.geometry_column_name
×
829

830
        if self.output_measurement:
×
831
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
×
832

833
        return {
×
834
            "type": self.name(),
835
            "params": params,
836
            "sources": {
837
                "vector": self.source.to_dict()
838
            }
839
        }
840

841
    @classmethod
1✔
842
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> VectorExpression:
1✔
843
        if operator_dict["type"] != "Expression":
×
844
            raise ValueError("Invalid operator type")
×
845

846
        geometry_column_name = None
×
847
        if "geometryColumnName" in operator_dict["params"]:
×
848
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
×
849

850
        output_measurement = None
×
851
        if "outputMeasurement" in operator_dict["params"]:
×
852
            output_measurement = Measurement.from_response(operator_dict["params"]["outputMeasurement"])
×
853

854
        return VectorExpression(
×
855
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
856
            expression=operator_dict["params"]["expression"],
857
            input_columns=operator_dict["params"]["inputColumns"],
858
            output_column=operator_dict["params"]["outputColumn"],
859
            geometry_column_name=geometry_column_name,
860
            output_measurement=output_measurement,
861
        )
862

863

864
class TemporalRasterAggregation(RasterOperator):
1✔
865
    '''A TemporalRasterAggregation operator.'''
866
    # pylint: disable=too-many-instance-attributes
867

868
    source: RasterOperator
1✔
869
    aggregation_type: Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"]
1✔
870
    ignore_no_data: bool = False
1✔
871
    window_granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days"
1✔
872
    window_size: int = 1
1✔
873
    output_type: Optional[Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]] = None
1✔
874
    percentile: Optional[float] = None
1✔
875
    window_ref: Optional[np.datetime64] = None
1✔
876

877
    # pylint: disable=too-many-arguments,too-many-positional-arguments
878
    def __init__(self,
1✔
879
                 source: RasterOperator,
880
                 aggregation_type:
881
                 Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"],
882
                 ignore_no_data: bool = False,
883
                 granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days",
884
                 window_size: int = 1,
885
                 output_type:
886
                 Optional[Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]] = None,
887
                 percentile: Optional[float] = None,
888
                 window_reference: Optional[Union[datetime.datetime, np.datetime64]] = None
889
                 ):
890
        '''Creates a new TemporalRasterAggregation operator.'''
891
        self.source = source
1✔
892
        self.aggregation_type = aggregation_type
1✔
893
        self.ignore_no_data = ignore_no_data
1✔
894
        self.window_granularity = granularity
1✔
895
        self.window_size = window_size
1✔
896
        self.output_type = output_type
1✔
897
        if self.aggregation_type == "percentileEstimate":
1✔
898
            if percentile is None:
×
899
                raise ValueError("Percentile must be set for percentileEstimate")
×
900
            if percentile <= 0.0 or percentile > 1.0:
×
901
                raise ValueError("Percentile must be > 0.0 and <= 1.0")
×
902
            self.percentile = percentile
×
903
        if window_reference is not None:
1✔
904
            if isinstance(window_reference, np.datetime64):
×
905
                self.window_ref = window_reference
×
906
            elif isinstance(window_reference, datetime.datetime):
×
907
                # We assume that a datetime without a timezone means UTC
908
                if window_reference.tzinfo is not None:
×
909
                    window_reference = window_reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
×
910
                self.window_ref = np.datetime64(window_reference)
×
911
            else:
912
                raise ValueError("`window_reference` must be of type `datetime.datetime` or `numpy.datetime64`")
×
913

914
    def name(self) -> str:
1✔
915
        return 'TemporalRasterAggregation'
1✔
916

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

920
        return {
1✔
921
            "type": self.name(),
922
            "params": {
923
                "aggregation": {
924
                    "type": self.aggregation_type,
925
                    "ignoreNoData": self.ignore_no_data,
926
                    "percentile": self.percentile
927
                },
928
                "window": {
929
                    "granularity": self.window_granularity,
930
                    "step": self.window_size
931
                },
932
                "windowReference": w_ref,
933
                "outputType": self.output_type
934
            },
935
            "sources": {
936
                "raster": self.source.to_dict()
937
            }
938
        }
939

940
    @classmethod
1✔
941
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'TemporalRasterAggregation':
1✔
942
        if operator_dict["type"] != "TemporalRasterAggregation":
1✔
943
            raise ValueError("Invalid operator type")
×
944

945
        w_ref: Optional[Union[datetime.datetime, np.datetime64]] = None
1✔
946
        if "windowReference" in operator_dict["params"]:
1✔
947
            t_ref = operator_dict["params"]["windowReference"]
1✔
948
            if isinstance(t_ref, str):
1✔
949
                w_ref = datetime.datetime.fromisoformat(t_ref)
×
950
            if isinstance(t_ref, int):
1✔
951
                w_ref = np.datetime64(t_ref, 'ms')
×
952

953
        percentile = None
1✔
954
        if "percentile" in operator_dict["params"]["aggregation"]:
1✔
955
            percentile = operator_dict["params"]["aggregation"]["percentile"]
1✔
956

957
        return TemporalRasterAggregation(
1✔
958
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
959
            aggregation_type=operator_dict["params"]["aggregation"]["type"],
960
            ignore_no_data=operator_dict["params"]["aggregation"]["ignoreNoData"],
961
            granularity=operator_dict["params"]["window"]["granularity"],
962
            window_size=operator_dict["params"]["window"]["step"],
963
            output_type=operator_dict["params"]["outputType"],
964
            window_reference=w_ref,
965
            percentile=percentile
966
        )
967

968

969
class TimeShift(Operator):
1✔
970
    '''A RasterTypeConversion operator.'''
971

972
    source: Union[RasterOperator, VectorOperator]
1✔
973
    shift_type: Literal["relative", "absolute"]
1✔
974
    granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"]
1✔
975
    value: int
1✔
976

977
    def __init__(self,
1✔
978
                 source: Union[RasterOperator, VectorOperator],
979
                 shift_type: Literal["relative", "absolute"],
980
                 granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"],
981
                 value: int,
982
                 ):
983
        '''Creates a new RasterTypeConversion operator.'''
984
        if shift_type == 'absolute':
1✔
985
            raise NotImplementedError("Absolute time shifts are not supported yet")
×
986
        self.source = source
1✔
987
        self.shift_type = shift_type
1✔
988
        self.granularity = granularity
1✔
989
        self.value = value
1✔
990

991
    def name(self) -> str:
1✔
992
        return 'TimeShift'
1✔
993

994
    def data_type(self) -> Literal['Vector', 'Raster']:
1✔
995
        return self.source.data_type()
×
996

997
    def as_vector(self) -> VectorOperator:
1✔
998
        '''Casts this operator to a VectorOperator.'''
999
        if self.data_type() != 'Vector':
×
1000
            raise TypeError("Cannot cast to VectorOperator")
×
1001
        return cast(VectorOperator, self)
×
1002

1003
    def as_raster(self) -> RasterOperator:
1✔
1004
        '''Casts this operator to a RasterOperator.'''
1005
        if self.data_type() != 'Raster':
×
1006
            raise TypeError("Cannot cast to RasterOperator")
×
1007
        return cast(RasterOperator, self)
×
1008

1009
    def to_dict(self) -> Dict[str, Any]:
1✔
1010
        return {
1✔
1011
            "type": self.name(),
1012
            "params": {
1013
                "type": self.shift_type,
1014
                "granularity": self.granularity,
1015
                "value": self.value
1016
            },
1017
            "sources": {
1018
                "source": self.source.to_dict()
1019
            }
1020
        }
1021

1022
    @classmethod
1✔
1023
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'TimeShift':
1✔
1024
        '''Constructs the operator from the given dictionary.'''
1025
        if operator_dict["type"] != "TimeShift":
×
1026
            raise ValueError("Invalid operator type")
×
1027
        source: Union[RasterOperator, VectorOperator]
1028
        try:
×
1029
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1030
        except ValueError:
×
1031
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1032

1033
        return TimeShift(
×
1034
            source=source,
1035
            shift_type=operator_dict["params"]["type"],
1036
            granularity=operator_dict["params"]["granularity"],
1037
            value=operator_dict["params"]["value"]
1038
        )
1039

1040

1041
class RenameBands:
1✔
1042
    '''Base class for renaming bands of a raster.'''
1043

1044
    @abstractmethod
1✔
1045
    def to_dict(self) -> Dict[str, Any]:
1✔
1046
        pass
×
1047

1048
    @classmethod
1✔
1049
    def from_dict(cls, rename_dict: Dict[str, Any]) -> 'RenameBands':
1✔
1050
        '''Returns a RenameBands object from a dictionary.'''
1051
        if rename_dict["type"] == "default":
×
1052
            return RenameBandsDefault()
×
1053
        if rename_dict["type"] == "suffix":
×
1054
            return RenameBandsSuffix(cast(List[str], rename_dict["values"]))
×
1055
        if rename_dict["type"] == "rename":
×
1056
            return RenameBandsRename(cast(List[str], rename_dict["values"]))
×
1057
        raise ValueError("Invalid rename type")
×
1058

1059
    @classmethod
1✔
1060
    def default(cls) -> 'RenameBands':
1✔
1061
        return RenameBandsDefault()
×
1062

1063
    @classmethod
1✔
1064
    def suffix(cls, values: List[str]) -> 'RenameBands':
1✔
1065
        return RenameBandsSuffix(values)
×
1066

1067
    @classmethod
1✔
1068
    def rename(cls, values: List[str]) -> 'RenameBands':
1✔
1069
        return RenameBandsRename(values)
×
1070

1071

1072
class RenameBandsDefault(RenameBands):
1✔
1073
    '''Rename bands with default suffix.'''
1074

1075
    def to_dict(self) -> Dict[str, Any]:
1✔
1076
        return {
1✔
1077
            "type": "default"
1078
        }
1079

1080

1081
class RenameBandsSuffix(RenameBands):
1✔
1082
    '''Rename bands with custom suffixes.'''
1083

1084
    suffixes: List[str]
1✔
1085

1086
    def __init__(self, suffixes: List[str]) -> None:
1✔
1087
        self.suffixes = suffixes
×
1088
        super().__init__()
×
1089

1090
    def to_dict(self) -> Dict[str, Any]:
1✔
1091
        return {
×
1092
            "type": "suffix",
1093
            "values": self.suffixes
1094
        }
1095

1096

1097
class RenameBandsRename(RenameBands):
1✔
1098
    '''Rename bands with new names.'''
1099

1100
    new_names: List[str]
1✔
1101

1102
    def __init__(self, new_names: List[str]) -> None:
1✔
1103
        self.new_names = new_names
×
1104
        super().__init__()
×
1105

1106
    def to_dict(self) -> Dict[str, Any]:
1✔
1107
        return {
×
1108
            "type": "rename",
1109
            "values": self.new_names
1110
        }
1111

1112

1113
class RasterStacker(RasterOperator):
1✔
1114
    '''The RasterStacker operator.'''
1115

1116
    sources: List[RasterOperator]
1✔
1117
    rename: RenameBands
1✔
1118

1119
    # pylint: disable=too-many-arguments
1120
    def __init__(self,
1✔
1121
                 sources: List[RasterOperator],
1122
                 rename: RenameBands = RenameBandsDefault()
1123
                 ):
1124
        '''Creates a new RasterStacker operator.'''
1125
        self.sources = sources
1✔
1126
        self.rename = rename
1✔
1127

1128
    def name(self) -> str:
1✔
1129
        return 'RasterStacker'
1✔
1130

1131
    def to_dict(self) -> Dict[str, Any]:
1✔
1132
        return {
1✔
1133
            "type": self.name(),
1134
            "params": {
1135
                "renameBands": self.rename.to_dict()
1136
            },
1137
            "sources": {
1138
                "rasters": [raster_source.to_dict() for raster_source in self.sources]
1139
            }
1140
        }
1141

1142
    @classmethod
1✔
1143
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'RasterStacker':
1✔
1144
        if operator_dict["type"] != "RasterStacker":
×
1145
            raise ValueError("Invalid operator type")
×
1146

1147
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
×
1148
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
×
1149

1150
        return RasterStacker(
×
1151
            sources=sources,
1152
            rename=rename
1153
        )
1154

1155

1156
class BandNeighborhoodAggregate(RasterOperator):
1✔
1157
    '''The BandNeighborhoodAggregate operator.'''
1158

1159
    source: RasterOperator
1✔
1160
    aggregate: BandNeighborhoodAggregateParams
1✔
1161

1162
    # pylint: disable=too-many-arguments
1163
    def __init__(self,
1✔
1164
                 source: RasterOperator,
1165
                 aggregate: BandNeighborhoodAggregateParams
1166
                 ):
1167
        '''Creates a new BandNeighborhoodAggregate operator.'''
1168
        self.source = source
×
1169
        self.aggregate = aggregate
×
1170

1171
    def name(self) -> str:
1✔
1172
        return 'BandNeighborhoodAggregate'
×
1173

1174
    def to_dict(self) -> Dict[str, Any]:
1✔
1175
        return {
×
1176
            "type": self.name(),
1177
            "params": {
1178
                "aggregate": self.aggregate.to_dict()
1179
            },
1180
            "sources": {
1181
                "raster": self.source.to_dict()
1182
            }
1183
        }
1184

1185
    @classmethod
1✔
1186
    def from_operator_dict(cls, operator_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregate':
1✔
1187
        if operator_dict["type"] != "BandNeighborhoodAggregate":
×
1188
            raise ValueError("Invalid operator type")
×
1189

1190
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1191
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1192

1193
        return BandNeighborhoodAggregate(
×
1194
            source=source,
1195
            aggregate=aggregate
1196
        )
1197

1198

1199
class BandNeighborhoodAggregateParams:
1✔
1200
    '''Abstract base class for band neighborhood aggregate params.'''
1201

1202
    @abstractmethod
1✔
1203
    def to_dict(self) -> Dict[str, Any]:
1✔
1204
        pass
×
1205

1206
    @classmethod
1✔
1207
    def from_dict(cls, band_neighborhood_aggregate_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregateParams':
1✔
1208
        '''Returns a BandNeighborhoodAggregate object from a dictionary.'''
1209
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
1210
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
1211
        if band_neighborhood_aggregate_dict["type"] == "average":
×
1212
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
1213
        raise ValueError("Invalid neighborhood aggregate type")
×
1214

1215
    @classmethod
1✔
1216
    def first_derivative(cls, equally_spaced_band_distance: float) -> 'BandNeighborhoodAggregateParams':
1✔
1217
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1218

1219
    @classmethod
1✔
1220
    def average(cls, window_size: int) -> 'BandNeighborhoodAggregateParams':
1✔
1221
        return BandNeighborhoodAggregateAverage(window_size)
×
1222

1223

1224
@dataclass
1✔
1225
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1✔
1226
    '''The first derivative band neighborhood aggregate.'''
1227

1228
    equally_spaced_band_distance: float
1✔
1229

1230
    @classmethod
1✔
1231
    def from_dict(cls, band_neighborhood_aggregate_dict: Dict[str, Any]) -> 'BandNeighborhoodAggregateParams':
1✔
1232
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
1233
            raise ValueError("Invalid neighborhood aggregate type")
×
1234

1235
        return BandNeighborhoodAggregateFirstDerivative(
×
1236
            band_neighborhood_aggregate_dict["bandDistance"]["distance"]
1237
        )
1238

1239
    def to_dict(self) -> Dict[str, Any]:
1✔
1240
        return {
×
1241
            "type": "firstDerivative",
1242
            "bandDistance": {
1243
                "type": "equallySpaced",
1244
                "distance": self.equally_spaced_band_distance
1245
            }
1246
        }
1247

1248

1249
@dataclass
1✔
1250
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1✔
1251
    '''The average band neighborhood aggregate.'''
1252

1253
    window_size: int
1✔
1254

1255
    def to_dict(self) -> Dict[str, Any]:
1✔
1256
        return {
×
1257
            "type": "average",
1258
            "windowSize": self.window_size
1259
        }
1260

1261

1262
class Onnx(RasterOperator):
1✔
1263
    '''Onnx ML operator.'''
1264

1265
    source: RasterOperator
1✔
1266
    model: str
1✔
1267

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

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

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

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

1296
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1297
        model = operator_dict["params"]["model"]
×
1298

1299
        return Onnx(
×
1300
            source=source,
1301
            model=model
1302
        )
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