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

geo-engine / geoengine-python / 19573839061

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

Pull #221

github

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

3083 of 3919 relevant lines covered (78.67%)

0.79 hits per line

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

69.0
geoengine/workflow_builder/operators.py
1
"""This module contains helpers to create workflow operators for the Geo Engine API."""
2

3
from __future__ import annotations
1✔
4

5
import datetime
1✔
6
from abc import abstractmethod
1✔
7
from dataclasses import dataclass
1✔
8
from enum import Enum
1✔
9
from typing import Any, Literal, cast
1✔
10

11
import geoengine_openapi_client
1✔
12
import numpy as np
1✔
13

14
from geoengine.datasets import DatasetName
1✔
15
from geoengine.types import Measurement, RasterBandDescriptor
1✔
16

17
# pylint: disable=too-many-lines
18

19

20
class Operator:
1✔
21
    """Base class for all operators."""
22

23
    @abstractmethod
1✔
24
    def name(self) -> str:
1✔
25
        """Returns the name of the operator."""
26

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

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

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

39
        return {
1✔
40
            "type": self.data_type(),
41
            "operator": self.to_dict(),
42
        }
43

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

52
        raise NotImplementedError(f"Unknown workflow type {workflow['type']}")
×
53

54

55
class RasterOperator(Operator):
1✔
56
    """Base class for all raster operators."""
57

58
    @abstractmethod
1✔
59
    def to_dict(self) -> dict[str, Any]:
1✔
60
        pass
×
61

62
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
63
        return "Raster"
1✔
64

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

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

93

94
class VectorOperator(Operator):
1✔
95
    """Base class for all vector operators."""
96

97
    @abstractmethod
1✔
98
    def to_dict(self) -> dict[str, Any]:
1✔
99
        pass
×
100

101
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
102
        return "Vector"
1✔
103

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

121

122
class GdalSource(RasterOperator):
1✔
123
    """A GDAL source operator."""
124

125
    dataset: str
1✔
126

127
    def __init__(self, dataset: str | DatasetName):
1✔
128
        """Creates a new GDAL source operator."""
129
        if isinstance(dataset, DatasetName):
1✔
130
            dataset = str(dataset)
1✔
131
        self.dataset = dataset
1✔
132

133
    def name(self) -> str:
1✔
134
        return "GdalSource"
1✔
135

136
    def to_dict(self) -> dict[str, Any]:
1✔
137
        return {"type": self.name(), "params": {"data": self.dataset}}
1✔
138

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

145
        return GdalSource(cast(str, operator_dict["params"]["data"]))
1✔
146

147

148
class OgrSource(VectorOperator):
1✔
149
    """An OGR source operator."""
150

151
    dataset: str
1✔
152
    attribute_projection: str | None = None
1✔
153
    attribute_filters: str | None = None
1✔
154

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

168
    def name(self) -> str:
1✔
169
        return "OgrSource"
1✔
170

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

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

187
        params = operator_dict["params"]
1✔
188
        return OgrSource(
1✔
189
            cast(str, params["data"]),
190
            attribute_projection=cast(str | None, params.get("attributeProjection")),
191
            attribute_filters=cast(str | None, params.get("attributeFilters")),
192
        )
193

194

195
class Interpolation(RasterOperator):
1✔
196
    """An interpolation operator."""
197

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
        # pylint: disable=too-many-arguments,too-many-positional-arguments
214
        """Creates a new interpolation operator."""
215
        self.source = source_operator
1✔
216
        self.interpolation = interpolation
1✔
217
        self.output_method = output_method
1✔
218
        self.output_x = output_x
1✔
219
        self.output_y = output_y
1✔
220

221
    def name(self) -> str:
1✔
222
        return "Interpolation"
1✔
223

224
    def to_dict(self) -> dict[str, Any]:
1✔
225
        if self.output_method == "fraction":
1✔
226
            input_res = {"type": "fraction", "x": self.output_x, "y": self.output_y}
1✔
227
        else:
228
            input_res = {"type": "resolution", "x": self.output_x, "y": self.output_y}
1✔
229

230
        return {
1✔
231
            "type": self.name(),
232
            "params": {"interpolation": self.interpolation, "outputResolution": input_res},
233
            "sources": {"raster": self.source.to_dict()},
234
        }
235

236
    @classmethod
1✔
237
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Interpolation:
1✔
238
        """Returns an operator from a dictionary."""
239
        if operator_dict["type"] != "Interpolation":
1✔
240
            raise ValueError("Invalid operator type")
×
241

242
        source = RasterOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["raster"]))
1✔
243

244
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
1✔
245
            output_x = float(params["x"])
1✔
246
            output_y = float(params["y"])
1✔
247

248
            if "type" not in params:
1✔
249
                raise KeyError("Interpolation outputResolution must contain a type: resolution OR fraction")
×
250
            if params["type"] == "fraction":
1✔
251
                return ("fraction", output_x, output_y)
1✔
252
            if params["type"] == "fraction":
×
253
                return ("fraction", output_x, output_y)
×
254
            raise ValueError(f"Invalid interpolation outputResolution type {params['type']}")
×
255

256
        (output_method, output_x, output_y) = parse_input_params(
1✔
257
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
258
        )
259

260
        return Interpolation(
1✔
261
            source_operator=source,
262
            output_method=output_method,
263
            output_x=output_x,
264
            output_y=output_y,
265
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict["params"]["interpolation"]),
266
        )
267

268

269
class Downsampling(RasterOperator):
1✔
270
    """A Downsampling operator."""
271

272
    source: RasterOperator
1✔
273
    sample_method: Literal["nearestNeighbor"] = "nearestNeighbor"
1✔
274
    output_method: Literal["resolution", "fraction"] = "resolution"
1✔
275
    output_x: float
1✔
276
    output_y: float
1✔
277
    # outputOriginReference: Optional[Coordinate2D]
278

279
    def __init__(
1✔
280
        self,
281
        source_operator: RasterOperator,
282
        output_x: float,
283
        output_y: float,
284
        output_method: Literal["resolution", "fraction"] = "resolution",
285
        sample_method: Literal["nearestNeighbor"] = "nearestNeighbor",
286
    ):
287
        # pylint: disable=too-many-arguments,too-many-positional-arguments
288
        """Creates a new Downsampling operator."""
289
        self.source = source_operator
×
290
        self.sample_method = sample_method
×
291
        self.output_method = output_method
×
292
        self.output_x = output_x
×
293
        self.output_y = output_y
×
294

295
    def name(self) -> str:
1✔
296
        return "Downsampling"
×
297

298
    def to_dict(self) -> dict[str, Any]:
1✔
299
        if self.output_method == "fraction":
×
300
            input_res = {"type": "fraction", "x": self.output_x, "y": self.output_y}
×
301
        else:
302
            input_res = {"type": "resolution", "x": self.output_x, "y": self.output_y}
×
303

304
        return {
×
305
            "type": self.name(),
306
            "params": {"samplingMethod": self.sample_method, "outputResolution": input_res},
307
            "sources": {"raster": self.source.to_dict()},
308
        }
309

310
    @classmethod
1✔
311
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Downsampling:
1✔
312
        """Returns an operator from a dictionary."""
313
        if operator_dict["type"] != "Downsampling":
×
314
            raise ValueError("Invalid operator type")
×
315

316
        source = RasterOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["raster"]))
×
317

318
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
×
319
            output_x = float(params["x"])
×
320
            output_y = float(params["y"])
×
321

322
            if "type" not in params:
×
323
                raise KeyError("Downsampling outputResolution must contain a type: resolution OR fraction")
×
324
            if params["type"] == "fraction":
×
325
                return ("fraction", output_x, output_y)
×
326
            if params["type"] == "fraction":
×
327
                return ("fraction", output_x, output_y)
×
328
            raise ValueError(f"Invalid Downsampling outputResolution type {params['type']}")
×
329

330
        (output_method, output_x, output_y) = parse_input_params(
×
331
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
332
        )
333

334
        return Downsampling(
×
335
            source_operator=source,
336
            output_method=output_method,
337
            output_x=output_x,
338
            output_y=output_y,
339
            sample_method=cast(Literal["nearestNeighbor"], operator_dict["params"]["downsampling"]),
340
        )
341

342

343
class ColumnNames:
1✔
344
    """Base class for deriving column names from bands of a raster."""
345

346
    @abstractmethod
1✔
347
    def to_dict(self) -> dict[str, Any]:
1✔
348
        pass
×
349

350
    @classmethod
1✔
351
    def from_dict(cls, rename_dict: dict[str, Any]) -> ColumnNames:
1✔
352
        """Returns a ColumnNames object from a dictionary."""
353
        if rename_dict["type"] == "default":
1✔
354
            return ColumnNamesDefault()
×
355
        if rename_dict["type"] == "suffix":
1✔
356
            return ColumnNamesSuffix(cast(list[str], rename_dict["values"]))
×
357
        if rename_dict["type"] == "names":
1✔
358
            return ColumnNamesNames(cast(list[str], rename_dict["values"]))
1✔
359
        raise ValueError("Invalid rename type")
×
360

361
    @classmethod
1✔
362
    def default(cls) -> ColumnNames:
1✔
363
        return ColumnNamesDefault()
×
364

365
    @classmethod
1✔
366
    def suffix(cls, values: list[str]) -> ColumnNames:
1✔
367
        return ColumnNamesSuffix(values)
×
368

369
    @classmethod
1✔
370
    def rename(cls, values: list[str]) -> ColumnNames:
1✔
371
        return ColumnNamesNames(values)
×
372

373

374
class ColumnNamesDefault(ColumnNames):
1✔
375
    """column names with default suffix."""
376

377
    def to_dict(self) -> dict[str, Any]:
1✔
378
        return {"type": "default"}
×
379

380

381
class ColumnNamesSuffix(ColumnNames):
1✔
382
    """Rename bands with custom suffixes."""
383

384
    suffixes: list[str]
1✔
385

386
    def __init__(self, suffixes: list[str]) -> None:
1✔
387
        self.suffixes = suffixes
×
388
        super().__init__()
×
389

390
    def to_dict(self) -> dict[str, Any]:
1✔
391
        return {"type": "suffix", "values": self.suffixes}
×
392

393

394
class ColumnNamesNames(ColumnNames):
1✔
395
    """Rename bands with new names."""
396

397
    new_names: list[str]
1✔
398

399
    def __init__(self, new_names: list[str]) -> None:
1✔
400
        self.new_names = new_names
1✔
401
        super().__init__()
1✔
402

403
    def to_dict(self) -> dict[str, Any]:
1✔
404
        return {"type": "names", "values": self.new_names}
1✔
405

406

407
class RasterVectorJoin(VectorOperator):
1✔
408
    """A RasterVectorJoin operator."""
409

410
    raster_sources: list[RasterOperator]
1✔
411
    vector_source: VectorOperator
1✔
412
    names: ColumnNames
1✔
413
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1✔
414
    temporal_aggregation_ignore_nodata: bool = False
1✔
415
    feature_aggregation: Literal["first", "mean"] = "mean"
1✔
416
    feature_aggregation_ignore_nodata: bool = False
1✔
417

418
    # pylint: disable=too-many-arguments,too-many-positional-arguments
419
    def __init__(
1✔
420
        self,
421
        raster_sources: list[RasterOperator],
422
        vector_source: VectorOperator,
423
        names: ColumnNames,
424
        temporal_aggregation: Literal["none", "first", "mean"] = "none",
425
        temporal_aggregation_ignore_nodata: bool = False,
426
        feature_aggregation: Literal["first", "mean"] = "mean",
427
        feature_aggregation_ignore_nodata: bool = False,
428
    ):
429
        """Creates a new RasterVectorJoin operator."""
430
        self.raster_source = raster_sources
1✔
431
        self.vector_source = vector_source
1✔
432
        self.names = names
1✔
433
        self.temporal_aggregation = temporal_aggregation
1✔
434
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1✔
435
        self.feature_aggregation = feature_aggregation
1✔
436
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1✔
437

438
    def name(self) -> str:
1✔
439
        return "RasterVectorJoin"
1✔
440

441
    def to_dict(self) -> dict[str, Any]:
1✔
442
        return {
1✔
443
            "type": self.name(),
444
            "params": {
445
                "names": self.names.to_dict(),
446
                "temporalAggregation": self.temporal_aggregation,
447
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
448
                "featureAggregation": self.feature_aggregation,
449
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
450
            },
451
            "sources": {
452
                "vector": self.vector_source.to_dict(),
453
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source],
454
            },
455
        }
456

457
    @classmethod
1✔
458
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterVectorJoin:
1✔
459
        """Returns an operator from a dictionary."""
460
        if operator_dict["type"] != "RasterVectorJoin":
1✔
461
            raise ValueError("Invalid operator type")
×
462

463
        vector_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["vector"]))
1✔
464
        raster_sources = [
1✔
465
            RasterOperator.from_operator_dict(raster_source)
466
            for raster_source in cast(list[dict[str, Any]], operator_dict["sources"]["rasters"])
467
        ]
468

469
        params = operator_dict["params"]
1✔
470
        return RasterVectorJoin(
1✔
471
            raster_sources=raster_sources,
472
            vector_source=vector_source,
473
            names=ColumnNames.from_dict(params["names"]),
474
            temporal_aggregation=cast(Literal["none", "first", "mean"], params["temporalAggregation"]),
475
            temporal_aggregation_ignore_nodata=cast(bool, params["temporalAggregationIgnoreNoData"]),
476
            feature_aggregation=cast(Literal["first", "mean"], params["featureAggregation"]),
477
            feature_aggregation_ignore_nodata=cast(bool, params["featureAggregationIgnoreNoData"]),
478
        )
479

480

481
class PointInPolygonFilter(VectorOperator):
1✔
482
    """A PointInPolygonFilter operator."""
483

484
    point_source: VectorOperator
1✔
485
    polygon_source: VectorOperator
1✔
486

487
    def __init__(
1✔
488
        self,
489
        point_source: VectorOperator,
490
        polygon_source: VectorOperator,
491
    ):
492
        """Creates a new PointInPolygonFilter filter operator."""
493
        self.point_source = point_source
1✔
494
        self.polygon_source = polygon_source
1✔
495

496
    def name(self) -> str:
1✔
497
        return "PointInPolygonFilter"
1✔
498

499
    def to_dict(self) -> dict[str, Any]:
1✔
500
        return {
1✔
501
            "type": self.name(),
502
            "params": {},
503
            "sources": {"points": self.point_source.to_dict(), "polygons": self.polygon_source.to_dict()},
504
        }
505

506
    @classmethod
1✔
507
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> PointInPolygonFilter:
1✔
508
        """Returns an operator from a dictionary."""
509
        if operator_dict["type"] != "PointInPolygonFilter":
1✔
510
            raise ValueError("Invalid operator type")
×
511

512
        point_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["points"]))
1✔
513
        polygon_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["polygons"]))
1✔
514

515
        return PointInPolygonFilter(
1✔
516
            point_source=point_source,
517
            polygon_source=polygon_source,
518
        )
519

520

521
class RasterScaling(RasterOperator):
1✔
522
    """A RasterScaling operator.
523

524
    This operator scales the values of a raster by a given slope and offset.
525

526
    The scaling is done as follows:
527
    y = (x - offset) / slope
528

529
    The unscale mode is the inverse of the scale mode:
530
    x = y * slope + offset
531

532
    """
533

534
    source: RasterOperator
1✔
535
    slope: float | str | None = None
1✔
536
    offset: float | str | None = None
1✔
537
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1✔
538
    output_measurement: str | None = None
1✔
539

540
    def __init__(
1✔
541
        self,
542
        # pylint: disable=too-many-arguments,too-many-positional-arguments
543
        source: RasterOperator,
544
        slope: float | str | None = None,
545
        offset: float | str | None = None,
546
        scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
547
        output_measurement: str | None = None,
548
    ):
549
        """Creates a new RasterScaling operator."""
550
        self.source = source
1✔
551
        self.slope = slope
1✔
552
        self.offset = offset
1✔
553
        self.scaling_mode = scaling_mode
1✔
554
        self.output_measurement = output_measurement
1✔
555
        if output_measurement is not None:
1✔
556
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
557

558
    def name(self) -> str:
1✔
559
        return "RasterScaling"
1✔
560

561
    def to_dict(self) -> dict[str, Any]:
1✔
562
        def offset_scale_dict(key_or_value: float | str | None) -> dict[str, Any]:
1✔
563
            if key_or_value is None:
1✔
564
                return {"type": "auto"}
1✔
565

566
            if isinstance(key_or_value, float):
1✔
567
                return {"type": "constant", "value": key_or_value}
1✔
568

569
            if isinstance(key_or_value, int):
×
570
                return {"type": "constant", "value": float(key_or_value)}
×
571

572
            # TODO: incorporate `domain` field
573
            return {"type": "metadataKey", "key": key_or_value}
×
574

575
        return {
1✔
576
            "type": self.name(),
577
            "params": {
578
                "offset": offset_scale_dict(self.offset),
579
                "slope": offset_scale_dict(self.slope),
580
                "scalingMode": self.scaling_mode,
581
            },
582
            "sources": {"raster": self.source.to_dict()},
583
        }
584

585
    @classmethod
1✔
586
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterScaling:
1✔
587
        if operator_dict["type"] != "RasterScaling":
1✔
588
            raise ValueError("Invalid operator type")
×
589

590
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
591
        params = operator_dict["params"]
1✔
592

593
        def offset_slope_reverse(key_or_value: dict[str, Any] | None) -> float | str | None:
1✔
594
            if key_or_value is None:
1✔
595
                return None
×
596
            if key_or_value["type"] == "constant":
1✔
597
                return key_or_value["value"]
1✔
598
            if key_or_value["type"] == "metadataKey":
1✔
599
                return key_or_value["key"]
×
600
            return None
1✔
601

602
        return RasterScaling(
1✔
603
            source_operator,
604
            slope=offset_slope_reverse(params["slope"]),
605
            offset=offset_slope_reverse(params["offset"]),
606
            scaling_mode=params["scalingMode"],
607
            output_measurement=params.get("outputMeasurement", None),
608
        )
609

610

611
class RasterTypeConversion(RasterOperator):
1✔
612
    """A RasterTypeConversion operator."""
613

614
    source: RasterOperator
1✔
615
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
616

617
    def __init__(
1✔
618
        self,
619
        source: RasterOperator,
620
        output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
621
    ):
622
        """Creates a new RasterTypeConversion operator."""
623
        self.source = source
1✔
624
        self.output_data_type = output_data_type
1✔
625

626
    def name(self) -> str:
1✔
627
        return "RasterTypeConversion"
1✔
628

629
    def to_dict(self) -> dict[str, Any]:
1✔
630
        return {
1✔
631
            "type": self.name(),
632
            "params": {"outputDataType": self.output_data_type},
633
            "sources": {"raster": self.source.to_dict()},
634
        }
635

636
    @classmethod
1✔
637
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterTypeConversion:
1✔
638
        if operator_dict["type"] != "RasterTypeConversion":
1✔
639
            raise ValueError("Invalid operator type")
×
640

641
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
642

643
        return RasterTypeConversion(source_operator, output_data_type=operator_dict["params"]["outputDataType"])
1✔
644

645

646
class Reprojection(Operator):
1✔
647
    """A Reprojection operator."""
648

649
    source: Operator
1✔
650
    target_spatial_reference: str
1✔
651

652
    def __init__(self, source: Operator, target_spatial_reference: str):
1✔
653
        """Creates a new Reprojection operator."""
654
        self.source = source
1✔
655
        self.target_spatial_reference = target_spatial_reference
1✔
656

657
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
658
        return self.source.data_type()
×
659

660
    def name(self) -> str:
1✔
661
        return "Reprojection"
1✔
662

663
    def to_dict(self) -> dict[str, Any]:
1✔
664
        return {
1✔
665
            "type": self.name(),
666
            "params": {"targetSpatialReference": self.target_spatial_reference},
667
            "sources": {"source": self.source.to_dict()},
668
        }
669

670
    def as_vector(self) -> VectorOperator:
1✔
671
        """Casts this operator to a VectorOperator."""
672
        if self.data_type() != "Vector":
×
673
            raise TypeError("Cannot cast to VectorOperator")
×
674
        return cast(VectorOperator, self)
×
675

676
    def as_raster(self) -> RasterOperator:
1✔
677
        """Casts this operator to a RasterOperator."""
678
        if self.data_type() != "Raster":
×
679
            raise TypeError("Cannot cast to RasterOperator")
×
680
        return cast(RasterOperator, self)
×
681

682
    @classmethod
1✔
683
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Reprojection:
1✔
684
        """Constructs the operator from the given dictionary."""
685
        if operator_dict["type"] != "Reprojection":
1✔
686
            raise ValueError("Invalid operator type")
×
687

688
        source_operator: RasterOperator | VectorOperator
689
        try:
1✔
690
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1✔
691
        except ValueError:
×
692
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
693

694
        return Reprojection(
1✔
695
            source=cast(Operator, source_operator),
696
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"],
697
        )
698

699

700
class Expression(RasterOperator):
1✔
701
    """An Expression operator."""
702

703
    expression: str
1✔
704
    source: RasterOperator
1✔
705
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
706
    map_no_data: bool = False
1✔
707
    output_band: RasterBandDescriptor | None = None
1✔
708

709
    # pylint: disable=too-many-arguments,too-many-positional-arguments
710
    def __init__(
1✔
711
        self,
712
        expression: str,
713
        source: RasterOperator,
714
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
715
        map_no_data: bool = False,
716
        output_band: RasterBandDescriptor | None = None,
717
    ):
718
        """Creates a new Expression operator."""
719
        self.expression = expression
1✔
720
        self.source = source
1✔
721
        self.output_type = output_type
1✔
722
        self.map_no_data = map_no_data
1✔
723
        self.output_band = output_band
1✔
724

725
    def name(self) -> str:
1✔
726
        return "Expression"
1✔
727

728
    def to_dict(self) -> dict[str, Any]:
1✔
729
        params = {
1✔
730
            "expression": self.expression,
731
            "outputType": self.output_type,
732
            "mapNoData": self.map_no_data,
733
        }
734
        if self.output_band is not None:
1✔
735
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1✔
736

737
        return {"type": self.name(), "params": params, "sources": {"raster": self.source.to_dict()}}
1✔
738

739
    @classmethod
1✔
740
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Expression:
1✔
741
        if operator_dict["type"] != "Expression":
1✔
742
            raise ValueError("Invalid operator type")
×
743

744
        output_band = None
1✔
745
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1✔
746
            raster_band_descriptor = geoengine_openapi_client.RasterBandDescriptor.from_dict(
1✔
747
                operator_dict["params"]["outputBand"]
748
            )
749
            if raster_band_descriptor is None:
1✔
750
                raise ValueError("Invalid output band")
×
751
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1✔
752

753
        return Expression(
1✔
754
            expression=operator_dict["params"]["expression"],
755
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
756
            output_type=operator_dict["params"]["outputType"],
757
            map_no_data=operator_dict["params"]["mapNoData"],
758
            output_band=output_band,
759
        )
760

761

762
class BandwiseExpression(RasterOperator):
1✔
763
    """A bandwise Expression operator."""
764

765
    expression: str
1✔
766
    source: RasterOperator
1✔
767
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
768
    map_no_data: bool = False
1✔
769

770
    # pylint: disable=too-many-arguments
771
    def __init__(
1✔
772
        self,
773
        expression: str,
774
        source: RasterOperator,
775
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
776
        map_no_data: bool = False,
777
    ):
778
        """Creates a new Expression operator."""
779
        self.expression = expression
×
780
        self.source = source
×
781
        self.output_type = output_type
×
782
        self.map_no_data = map_no_data
×
783

784
    def name(self) -> str:
1✔
785
        return "BandwiseExpression"
×
786

787
    def to_dict(self) -> dict[str, Any]:
1✔
788
        params = {
×
789
            "expression": self.expression,
790
            "outputType": self.output_type,
791
            "mapNoData": self.map_no_data,
792
        }
793

794
        return {"type": self.name(), "params": params, "sources": {"raster": self.source.to_dict()}}
×
795

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

801
        return BandwiseExpression(
×
802
            expression=operator_dict["params"]["expression"],
803
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
804
            output_type=operator_dict["params"]["outputType"],
805
            map_no_data=operator_dict["params"]["mapNoData"],
806
        )
807

808

809
class GeoVectorDataType(Enum):
1✔
810
    """The output type of geometry vector data."""
811

812
    MULTI_POINT = "MultiPoint"
1✔
813
    MULTI_LINE_STRING = "MultiLineString"
1✔
814
    MULTI_POLYGON = "MultiPolygon"
1✔
815

816

817
class VectorExpression(VectorOperator):
1✔
818
    """The `VectorExpression` operator."""
819

820
    source: VectorOperator
1✔
821

822
    expression: str
1✔
823
    input_columns: list[str]
1✔
824
    output_column: str | GeoVectorDataType
1✔
825
    geometry_column_name = None
1✔
826
    output_measurement: Measurement | None = None
1✔
827

828
    # pylint: disable=too-many-arguments
829
    def __init__(
1✔
830
        self,
831
        source: VectorOperator,
832
        *,
833
        expression: str,
834
        input_columns: list[str],
835
        output_column: str | GeoVectorDataType,
836
        geometry_column_name: str | None = None,
837
        output_measurement: Measurement | None = None,
838
    ):
839
        """Creates a new VectorExpression operator."""
840
        self.source = source
1✔
841

842
        self.expression = expression
1✔
843
        self.input_columns = input_columns
1✔
844
        self.output_column = output_column
1✔
845

846
        self.geometry_column_name = geometry_column_name
1✔
847
        self.output_measurement = output_measurement
1✔
848

849
    def name(self) -> str:
1✔
850
        return "VectorExpression"
1✔
851

852
    def to_dict(self) -> dict[str, Any]:
1✔
853
        output_column_dict = None
1✔
854
        if isinstance(self.output_column, GeoVectorDataType):
1✔
855
            output_column_dict = {
1✔
856
                "type": "geometry",
857
                "value": self.output_column.value,
858
            }
859
        elif isinstance(self.output_column, str):
1✔
860
            output_column_dict = {
1✔
861
                "type": "column",
862
                "value": self.output_column,
863
            }
864
        else:
865
            raise NotImplementedError("Invalid output column type")
×
866

867
        params = {
1✔
868
            "expression": self.expression,
869
            "inputColumns": self.input_columns,
870
            "outputColumn": output_column_dict,
871
        }  # type: dict[str, Any]
872

873
        if self.geometry_column_name:
1✔
874
            params["geometryColumnName"] = self.geometry_column_name
×
875

876
        if self.output_measurement:
1✔
877
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
×
878

879
        return {"type": self.name(), "params": params, "sources": {"vector": self.source.to_dict()}}
1✔
880

881
    @classmethod
1✔
882
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> VectorExpression:
1✔
883
        if operator_dict["type"] != "Expression":
×
884
            raise ValueError("Invalid operator type")
×
885

886
        geometry_column_name = None
×
887
        if "geometryColumnName" in operator_dict["params"]:
×
888
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
×
889

890
        output_measurement = None
×
891
        if "outputMeasurement" in operator_dict["params"]:
×
892
            output_measurement = Measurement.from_response(operator_dict["params"]["outputMeasurement"])
×
893

894
        return VectorExpression(
×
895
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
896
            expression=operator_dict["params"]["expression"],
897
            input_columns=operator_dict["params"]["inputColumns"],
898
            output_column=operator_dict["params"]["outputColumn"],
899
            geometry_column_name=geometry_column_name,
900
            output_measurement=output_measurement,
901
        )
902

903

904
class TemporalRasterAggregation(RasterOperator):
1✔
905
    """A TemporalRasterAggregation operator."""
906

907
    # pylint: disable=too-many-instance-attributes
908

909
    source: RasterOperator
1✔
910
    aggregation_type: Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"]
1✔
911
    ignore_no_data: bool = False
1✔
912
    window_granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days"
1✔
913
    window_size: int = 1
1✔
914
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None
1✔
915
    percentile: float | None = None
1✔
916
    window_ref: np.datetime64 | None = None
1✔
917

918
    # pylint: disable=too-many-arguments,too-many-positional-arguments
919
    def __init__(
1✔
920
        self,
921
        source: RasterOperator,
922
        aggregation_type: Literal[
923
            "mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"
924
        ],
925
        ignore_no_data: bool = False,
926
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days",
927
        window_size: int = 1,
928
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None,
929
        percentile: float | None = None,
930
        window_reference: datetime.datetime | np.datetime64 | None = None,
931
    ):
932
        """Creates a new TemporalRasterAggregation operator."""
933
        self.source = source
1✔
934
        self.aggregation_type = aggregation_type
1✔
935
        self.ignore_no_data = ignore_no_data
1✔
936
        self.window_granularity = granularity
1✔
937
        self.window_size = window_size
1✔
938
        self.output_type = output_type
1✔
939
        if self.aggregation_type == "percentileEstimate":
1✔
940
            if percentile is None:
×
941
                raise ValueError("Percentile must be set for percentileEstimate")
×
942
            if percentile <= 0.0 or percentile > 1.0:
×
943
                raise ValueError("Percentile must be > 0.0 and <= 1.0")
×
944
            self.percentile = percentile
×
945
        if window_reference is not None:
1✔
946
            if isinstance(window_reference, np.datetime64):
×
947
                self.window_ref = window_reference
×
948
            elif isinstance(window_reference, datetime.datetime):
×
949
                # We assume that a datetime without a timezone means UTC
950
                if window_reference.tzinfo is not None:
×
951
                    window_reference = window_reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
×
952
                self.window_ref = np.datetime64(window_reference)
×
953
            else:
954
                raise ValueError("`window_reference` must be of type `datetime.datetime` or `numpy.datetime64`")
×
955

956
    def name(self) -> str:
1✔
957
        return "TemporalRasterAggregation"
1✔
958

959
    def to_dict(self) -> dict[str, Any]:
1✔
960
        w_ref = self.window_ref.astype("datetime64[ms]").astype(int) if self.window_ref is not None else None
1✔
961

962
        return {
1✔
963
            "type": self.name(),
964
            "params": {
965
                "aggregation": {
966
                    "type": self.aggregation_type,
967
                    "ignoreNoData": self.ignore_no_data,
968
                    "percentile": self.percentile,
969
                },
970
                "window": {"granularity": self.window_granularity, "step": self.window_size},
971
                "windowReference": w_ref,
972
                "outputType": self.output_type,
973
            },
974
            "sources": {"raster": self.source.to_dict()},
975
        }
976

977
    @classmethod
1✔
978
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TemporalRasterAggregation:
1✔
979
        if operator_dict["type"] != "TemporalRasterAggregation":
1✔
980
            raise ValueError("Invalid operator type")
×
981

982
        w_ref: datetime.datetime | np.datetime64 | None = None
1✔
983
        if "windowReference" in operator_dict["params"]:
1✔
984
            t_ref = operator_dict["params"]["windowReference"]
1✔
985
            if isinstance(t_ref, str):
1✔
986
                w_ref = datetime.datetime.fromisoformat(t_ref)
×
987
            if isinstance(t_ref, int):
1✔
988
                w_ref = np.datetime64(t_ref, "ms")
×
989

990
        percentile = None
1✔
991
        if "percentile" in operator_dict["params"]["aggregation"]:
1✔
992
            percentile = operator_dict["params"]["aggregation"]["percentile"]
1✔
993

994
        return TemporalRasterAggregation(
1✔
995
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
996
            aggregation_type=operator_dict["params"]["aggregation"]["type"],
997
            ignore_no_data=operator_dict["params"]["aggregation"]["ignoreNoData"],
998
            granularity=operator_dict["params"]["window"]["granularity"],
999
            window_size=operator_dict["params"]["window"]["step"],
1000
            output_type=operator_dict["params"]["outputType"],
1001
            window_reference=w_ref,
1002
            percentile=percentile,
1003
        )
1004

1005

1006
class TimeShift(Operator):
1✔
1007
    """A RasterTypeConversion operator."""
1008

1009
    source: RasterOperator | VectorOperator
1✔
1010
    shift_type: Literal["relative", "absolute"]
1✔
1011
    granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"]
1✔
1012
    value: int
1✔
1013

1014
    def __init__(
1✔
1015
        self,
1016
        source: RasterOperator | VectorOperator,
1017
        shift_type: Literal["relative", "absolute"],
1018
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"],
1019
        value: int,
1020
    ):
1021
        """Creates a new RasterTypeConversion operator."""
1022
        if shift_type == "absolute":
1✔
1023
            raise NotImplementedError("Absolute time shifts are not supported yet")
×
1024
        self.source = source
1✔
1025
        self.shift_type = shift_type
1✔
1026
        self.granularity = granularity
1✔
1027
        self.value = value
1✔
1028

1029
    def name(self) -> str:
1✔
1030
        return "TimeShift"
1✔
1031

1032
    def data_type(self) -> Literal["Vector", "Raster"]:
1✔
1033
        return self.source.data_type()
×
1034

1035
    def as_vector(self) -> VectorOperator:
1✔
1036
        """Casts this operator to a VectorOperator."""
1037
        if self.data_type() != "Vector":
×
1038
            raise TypeError("Cannot cast to VectorOperator")
×
1039
        return cast(VectorOperator, self)
×
1040

1041
    def as_raster(self) -> RasterOperator:
1✔
1042
        """Casts this operator to a RasterOperator."""
1043
        if self.data_type() != "Raster":
×
1044
            raise TypeError("Cannot cast to RasterOperator")
×
1045
        return cast(RasterOperator, self)
×
1046

1047
    def to_dict(self) -> dict[str, Any]:
1✔
1048
        return {
1✔
1049
            "type": self.name(),
1050
            "params": {"type": self.shift_type, "granularity": self.granularity, "value": self.value},
1051
            "sources": {"source": self.source.to_dict()},
1052
        }
1053

1054
    @classmethod
1✔
1055
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TimeShift:
1✔
1056
        """Constructs the operator from the given dictionary."""
1057
        if operator_dict["type"] != "TimeShift":
×
1058
            raise ValueError("Invalid operator type")
×
1059
        source: RasterOperator | VectorOperator
1060
        try:
×
1061
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1062
        except ValueError:
×
1063
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1064

1065
        return TimeShift(
×
1066
            source=source,
1067
            shift_type=operator_dict["params"]["type"],
1068
            granularity=operator_dict["params"]["granularity"],
1069
            value=operator_dict["params"]["value"],
1070
        )
1071

1072

1073
class RenameBands:
1✔
1074
    """Base class for renaming bands of a raster."""
1075

1076
    @abstractmethod
1✔
1077
    def to_dict(self) -> dict[str, Any]:
1✔
1078
        pass
×
1079

1080
    @classmethod
1✔
1081
    def from_dict(cls, rename_dict: dict[str, Any]) -> RenameBands:
1✔
1082
        """Returns a RenameBands object from a dictionary."""
1083
        if rename_dict["type"] == "default":
×
1084
            return RenameBandsDefault()
×
1085
        if rename_dict["type"] == "suffix":
×
1086
            return RenameBandsSuffix(cast(list[str], rename_dict["values"]))
×
1087
        if rename_dict["type"] == "rename":
×
1088
            return RenameBandsRename(cast(list[str], rename_dict["values"]))
×
1089
        raise ValueError("Invalid rename type")
×
1090

1091
    @classmethod
1✔
1092
    def default(cls) -> RenameBands:
1✔
1093
        return RenameBandsDefault()
×
1094

1095
    @classmethod
1✔
1096
    def suffix(cls, values: list[str]) -> RenameBands:
1✔
1097
        return RenameBandsSuffix(values)
×
1098

1099
    @classmethod
1✔
1100
    def rename(cls, values: list[str]) -> RenameBands:
1✔
1101
        return RenameBandsRename(values)
×
1102

1103

1104
class RenameBandsDefault(RenameBands):
1✔
1105
    """Rename bands with default suffix."""
1106

1107
    def to_dict(self) -> dict[str, Any]:
1✔
1108
        return {"type": "default"}
1✔
1109

1110

1111
class RenameBandsSuffix(RenameBands):
1✔
1112
    """Rename bands with custom suffixes."""
1113

1114
    suffixes: list[str]
1✔
1115

1116
    def __init__(self, suffixes: list[str]) -> None:
1✔
1117
        self.suffixes = suffixes
×
1118
        super().__init__()
×
1119

1120
    def to_dict(self) -> dict[str, Any]:
1✔
1121
        return {"type": "suffix", "values": self.suffixes}
×
1122

1123

1124
class RenameBandsRename(RenameBands):
1✔
1125
    """Rename bands with new names."""
1126

1127
    new_names: list[str]
1✔
1128

1129
    def __init__(self, new_names: list[str]) -> None:
1✔
1130
        self.new_names = new_names
×
1131
        super().__init__()
×
1132

1133
    def to_dict(self) -> dict[str, Any]:
1✔
1134
        return {"type": "rename", "values": self.new_names}
×
1135

1136

1137
class RasterStacker(RasterOperator):
1✔
1138
    """The RasterStacker operator."""
1139

1140
    sources: list[RasterOperator]
1✔
1141
    rename: RenameBands
1✔
1142

1143
    # pylint: disable=too-many-arguments
1144
    def __init__(self, sources: list[RasterOperator], rename: RenameBands | None = None):
1✔
1145
        """Creates a new RasterStacker operator."""
1146
        if rename is None:
1✔
1147
            rename = RenameBandsDefault()
1✔
1148

1149
        self.sources = sources
1✔
1150
        self.rename = rename
1✔
1151

1152
    def name(self) -> str:
1✔
1153
        return "RasterStacker"
1✔
1154

1155
    def to_dict(self) -> dict[str, Any]:
1✔
1156
        return {
1✔
1157
            "type": self.name(),
1158
            "params": {"renameBands": self.rename.to_dict()},
1159
            "sources": {"rasters": [raster_source.to_dict() for raster_source in self.sources]},
1160
        }
1161

1162
    @classmethod
1✔
1163
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterStacker:
1✔
1164
        if operator_dict["type"] != "RasterStacker":
×
1165
            raise ValueError("Invalid operator type")
×
1166

1167
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
×
1168
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
×
1169

1170
        return RasterStacker(sources=sources, rename=rename)
×
1171

1172

1173
class BandNeighborhoodAggregate(RasterOperator):
1✔
1174
    """The BandNeighborhoodAggregate operator."""
1175

1176
    source: RasterOperator
1✔
1177
    aggregate: BandNeighborhoodAggregateParams
1✔
1178

1179
    # pylint: disable=too-many-arguments
1180
    def __init__(self, source: RasterOperator, aggregate: BandNeighborhoodAggregateParams):
1✔
1181
        """Creates a new BandNeighborhoodAggregate operator."""
1182
        self.source = source
×
1183
        self.aggregate = aggregate
×
1184

1185
    def name(self) -> str:
1✔
1186
        return "BandNeighborhoodAggregate"
×
1187

1188
    def to_dict(self) -> dict[str, Any]:
1✔
1189
        return {
×
1190
            "type": self.name(),
1191
            "params": {"aggregate": self.aggregate.to_dict()},
1192
            "sources": {"raster": self.source.to_dict()},
1193
        }
1194

1195
    @classmethod
1✔
1196
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandNeighborhoodAggregate:
1✔
1197
        if operator_dict["type"] != "BandNeighborhoodAggregate":
×
1198
            raise ValueError("Invalid operator type")
×
1199

1200
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1201
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1202

1203
        return BandNeighborhoodAggregate(source=source, aggregate=aggregate)
×
1204

1205

1206
class BandNeighborhoodAggregateParams:
1✔
1207
    """Abstract base class for band neighborhood aggregate params."""
1208

1209
    @abstractmethod
1✔
1210
    def to_dict(self) -> dict[str, Any]:
1✔
1211
        pass
×
1212

1213
    @classmethod
1✔
1214
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1215
        """Returns a BandNeighborhoodAggregate object from a dictionary."""
1216
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
1217
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
1218
        if band_neighborhood_aggregate_dict["type"] == "average":
×
1219
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
1220
        raise ValueError("Invalid neighborhood aggregate type")
×
1221

1222
    @classmethod
1✔
1223
    def first_derivative(cls, equally_spaced_band_distance: float) -> BandNeighborhoodAggregateParams:
1✔
1224
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1225

1226
    @classmethod
1✔
1227
    def average(cls, window_size: int) -> BandNeighborhoodAggregateParams:
1✔
1228
        return BandNeighborhoodAggregateAverage(window_size)
×
1229

1230

1231
@dataclass
1✔
1232
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1✔
1233
    """The first derivative band neighborhood aggregate."""
1234

1235
    equally_spaced_band_distance: float
1✔
1236

1237
    @classmethod
1✔
1238
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1239
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
1240
            raise ValueError("Invalid neighborhood aggregate type")
×
1241

1242
        return BandNeighborhoodAggregateFirstDerivative(band_neighborhood_aggregate_dict["bandDistance"]["distance"])
×
1243

1244
    def to_dict(self) -> dict[str, Any]:
1✔
1245
        return {
×
1246
            "type": "firstDerivative",
1247
            "bandDistance": {"type": "equallySpaced", "distance": self.equally_spaced_band_distance},
1248
        }
1249

1250

1251
@dataclass
1✔
1252
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1✔
1253
    """The average band neighborhood aggregate."""
1254

1255
    window_size: int
1✔
1256

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

1260

1261
class Onnx(RasterOperator):
1✔
1262
    """Onnx ML operator."""
1263

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

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

1273
    def name(self) -> str:
1✔
1274
        return "Onnx"
×
1275

1276
    def to_dict(self) -> dict[str, Any]:
1✔
1277
        return {"type": self.name(), "params": {"model": self.model}, "sources": {"raster": self.source.to_dict()}}
×
1278

1279
    @classmethod
1✔
1280
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Onnx:
1✔
1281
        if operator_dict["type"] != "Onnx":
×
1282
            raise ValueError("Invalid operator type")
×
1283

1284
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1285
        model = operator_dict["params"]["model"]
×
1286

1287
        return Onnx(source=source, model=model)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc