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

geo-engine / geoengine / 27023555140

05 Jun 2026 03:20PM UTC coverage: 87.318% (+0.1%) from 87.218%
27023555140

Pull #1199

github

web-flow
Merge cfe7b0044 into 255ac7144
Pull Request #1199: fix: add resolution to python down/up sampling operators

13 of 30 new or added lines in 4 files covered. (43.33%)

6 existing lines in 3 files now uncovered.

117267 of 134299 relevant lines covered (87.32%)

482078.82 hits per line

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

77.37
/python/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_api_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":
1✔
50
            return VectorOperator.from_operator_dict(workflow["operator"])
1✔
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()
×
NEW
76
        if operator_dict["type"] == "Downsampling":
×
NEW
77
            return Downsampling.from_operator_dict(operator_dict)
×
78
        if operator_dict["type"] == "Interpolation":
×
79
            return Interpolation.from_operator_dict(operator_dict)
×
80
        if operator_dict["type"] == "Expression":
×
81
            return Expression.from_operator_dict(operator_dict)
×
82
        if operator_dict["type"] == "BandwiseExpression":
×
83
            return BandwiseExpression.from_operator_dict(operator_dict)
×
84
        if operator_dict["type"] == "TimeShift":
×
85
            return TimeShift.from_operator_dict(operator_dict).as_raster()
×
86
        if operator_dict["type"] == "TemporalRasterAggregation":
×
87
            return TemporalRasterAggregation.from_operator_dict(operator_dict)
×
88
        if operator_dict["type"] == "RasterStacker":
×
89
            return RasterStacker.from_operator_dict(operator_dict)
×
90
        if operator_dict["type"] == "BandNeighborhoodAggregate":
×
91
            return BandNeighborhoodAggregate.from_operator_dict(operator_dict)
×
92

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

95

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

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

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

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

123

124
class GdalSource(RasterOperator):
1✔
125
    """A GDAL source operator."""
126

127
    dataset: str
1✔
128

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

135
    def name(self) -> str:
1✔
136
        return "GdalSource"
1✔
137

138
    def to_dict(self) -> dict[str, Any]:
1✔
139
        return {"type": self.name(), "params": {"data": self.dataset}}
1✔
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

153
    dataset: str
1✔
154
    attribute_projection: str | None = None
1✔
155
    attribute_filters: str | None = None
1✔
156

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

170
    def name(self) -> str:
1✔
171
        return "OgrSource"
1✔
172

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

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

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

196

197
class Interpolation(RasterOperator):
1✔
198
    """An interpolation operator."""
199

200
    source: RasterOperator
1✔
201
    interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear"
1✔
202
    output_method: Literal["resolution", "fraction"] = "resolution"
1✔
203
    output_x: float
1✔
204
    output_y: float
1✔
205
    output_origin_reference: tuple[float, float] | None
1✔
206

207
    def __init__(
1✔
208
        self,
209
        source_operator: RasterOperator,
210
        output_x: float,
211
        output_y: float,
212
        output_method: Literal["resolution", "fraction"] = "resolution",
213
        interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear",
214
        output_origin_reference: tuple[float, float] | None = None,
215
    ):
216
        # pylint: disable=too-many-arguments,too-many-positional-arguments
217
        """Creates a new interpolation operator."""
218
        self.source = source_operator
1✔
219
        self.interpolation = interpolation
1✔
220
        self.output_method = output_method
1✔
221
        self.output_x = output_x
1✔
222
        self.output_y = output_y
1✔
223
        self.output_origin_reference = output_origin_reference
1✔
224

225
    def name(self) -> str:
1✔
226
        return "Interpolation"
1✔
227

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

234
        if self.output_origin_reference is not None:
1✔
NEW
235
            return {
×
236
                "type": self.name(),
237
                "params": {
238
                    "interpolation": self.interpolation,
239
                    "outputResolution": input_res,
240
                    "outputOriginReference": {
241
                        "x": self.output_origin_reference[0],
242
                        "y": self.output_origin_reference[1],
243
                    },
244
                },
245
                "sources": {"raster": self.source.to_dict()},
246
            }
247

248
        return {
1✔
249
            "type": self.name(),
250
            "params": {
251
                "interpolation": self.interpolation,
252
                "outputResolution": input_res,
253
            },
254
            "sources": {"raster": self.source.to_dict()},
255
        }
256

257
    @classmethod
1✔
258
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Interpolation:
1✔
259
        """Returns an operator from a dictionary."""
260
        if operator_dict["type"] != "Interpolation":
1✔
261
            raise ValueError("Invalid operator type")
×
262

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

265
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
1✔
266
            output_x = float(params["x"])
1✔
267
            output_y = float(params["y"])
1✔
268

269
            if "type" not in params:
1✔
270
                raise KeyError("Interpolation outputResolution must contain a type: resolution OR fraction")
×
271
            if params["type"] == "fraction":
1✔
272
                return ("fraction", output_x, output_y)
1✔
NEW
273
            if params["type"] == "resolution":
×
NEW
274
                return ("resolution", output_x, output_y)
×
UNCOV
275
            raise ValueError(f"Invalid interpolation outputResolution type {params['type']}")
×
276

277
        (output_method, output_x, output_y) = parse_input_params(
1✔
278
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
279
        )
280

281
        op = Interpolation(
1✔
282
            source_operator=source,
283
            output_method=output_method,
284
            output_x=output_x,
285
            output_y=output_y,
286
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict["params"]["interpolation"]),
287
        )
288

289
        if (
1✔
290
            "outputOriginReference" in operator_dict["params"]
291
            and operator_dict["params"]["outputOriginReference"] is not None
292
        ):
NEW
293
            op.output_origin_reference = (
×
294
                float(operator_dict["params"]["outputOriginReference"]["x"]),
295
                float(operator_dict["params"]["outputOriginReference"]["y"]),
296
            )
297

298
        return op
1✔
299

300

301
class Downsampling(RasterOperator):
1✔
302
    """A Downsampling operator."""
303

304
    source: RasterOperator
1✔
305
    sample_method: Literal["nearestNeighbor"] = "nearestNeighbor"
1✔
306
    output_method: Literal["resolution", "fraction"] = "resolution"
1✔
307
    output_x: float
1✔
308
    output_y: float
1✔
309
    output_origin_reference: tuple[float, float] | None
1✔
310

311
    def __init__(
1✔
312
        self,
313
        source_operator: RasterOperator,
314
        output_x: float,
315
        output_y: float,
316
        output_method: Literal["resolution", "fraction"] = "resolution",
317
        sample_method: Literal["nearestNeighbor"] = "nearestNeighbor",
318
        output_origin_reference: tuple[float, float] | None = None,
319
    ):
320
        # pylint: disable=too-many-arguments,too-many-positional-arguments
321
        """Creates a new Downsampling operator."""
322
        self.source = source_operator
1✔
323
        self.sample_method = sample_method
1✔
324
        self.output_method = output_method
1✔
325
        self.output_x = output_x
1✔
326
        self.output_y = output_y
1✔
327
        self.output_origin_reference = output_origin_reference
1✔
328

329
    def name(self) -> str:
1✔
330
        return "Downsampling"
1✔
331

332
    def to_dict(self) -> dict[str, Any]:
1✔
333
        if self.output_method == "fraction":
1✔
334
            input_res = {"type": "fraction", "x": self.output_x, "y": self.output_y}
1✔
335
        else:
336
            input_res = {"type": "resolution", "x": self.output_x, "y": self.output_y}
×
337

338
        if self.output_origin_reference is not None:
1✔
NEW
339
            return {
×
340
                "type": self.name(),
341
                "params": {
342
                    "samplingMethod": self.sample_method,
343
                    "outputResolution": input_res,
344
                    "outputOriginReference": {
345
                        "x": self.output_origin_reference[0],
346
                        "y": self.output_origin_reference[1],
347
                    },
348
                },
349
                "sources": {"raster": self.source.to_dict()},
350
            }
351

352
        return {
1✔
353
            "type": self.name(),
354
            "params": {
355
                "samplingMethod": self.sample_method,
356
                "outputResolution": input_res,
357
            },
358
            "sources": {"raster": self.source.to_dict()},
359
        }
360

361
    @classmethod
1✔
362
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Downsampling:
1✔
363
        """Returns an operator from a dictionary."""
364
        if operator_dict["type"] != "Downsampling":
1✔
365
            raise ValueError("Invalid operator type")
×
366

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

369
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
1✔
370
            output_x = float(params["x"])
1✔
371
            output_y = float(params["y"])
1✔
372

373
            if "type" not in params:
1✔
374
                raise KeyError("Downsampling outputResolution must contain a type: resolution OR fraction")
×
375
            if params["type"] == "fraction":
1✔
376
                return ("fraction", output_x, output_y)
1✔
NEW
377
            if params["type"] == "resolution":
×
NEW
378
                return ("resolution", output_x, output_y)
×
UNCOV
379
            raise ValueError(f"Invalid Downsampling outputResolution type {params['type']}")
×
380

381
        (output_method, output_x, output_y) = parse_input_params(
1✔
382
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
383
        )
384

385
        op = Downsampling(
1✔
386
            source_operator=source,
387
            output_method=output_method,
388
            output_x=output_x,
389
            output_y=output_y,
390
            sample_method=cast(Literal["nearestNeighbor"], operator_dict["params"]["samplingMethod"]),
391
            output_origin_reference=None,
392
        )
393

394
        if (
1✔
395
            "outputOriginReference" in operator_dict["params"]
396
            and operator_dict["params"]["outputOriginReference"] is not None
397
        ):
NEW
398
            op.output_origin_reference = (
×
399
                float(operator_dict["params"]["outputOriginReference"]["x"]),
400
                float(operator_dict["params"]["outputOriginReference"]["y"]),
401
            )
402

403
        return op
1✔
404

405

406
class ColumnNames:
1✔
407
    """Base class for deriving column names from bands of a raster."""
408

409
    @abstractmethod
1✔
410
    def to_dict(self) -> dict[str, Any]:
1✔
411
        pass
×
412

413
    @classmethod
1✔
414
    def from_dict(cls, rename_dict: dict[str, Any]) -> ColumnNames:
1✔
415
        """Returns a ColumnNames object from a dictionary."""
416
        if rename_dict["type"] == "default":
1✔
417
            return ColumnNamesDefault()
×
418
        if rename_dict["type"] == "suffix":
1✔
419
            return ColumnNamesSuffix(cast(list[str], rename_dict["values"]))
×
420
        if rename_dict["type"] == "names":
1✔
421
            return ColumnNamesNames(cast(list[str], rename_dict["values"]))
1✔
422
        raise ValueError("Invalid rename type")
×
423

424
    @classmethod
1✔
425
    def default(cls) -> ColumnNames:
1✔
426
        return ColumnNamesDefault()
×
427

428
    @classmethod
1✔
429
    def suffix(cls, values: list[str]) -> ColumnNames:
1✔
430
        return ColumnNamesSuffix(values)
×
431

432
    @classmethod
1✔
433
    def rename(cls, values: list[str]) -> ColumnNames:
1✔
434
        return ColumnNamesNames(values)
×
435

436

437
class ColumnNamesDefault(ColumnNames):
1✔
438
    """column names with default suffix."""
439

440
    def to_dict(self) -> dict[str, Any]:
1✔
441
        return {"type": "default"}
×
442

443

444
class ColumnNamesSuffix(ColumnNames):
1✔
445
    """Rename bands with custom suffixes."""
446

447
    suffixes: list[str]
1✔
448

449
    def __init__(self, suffixes: list[str]) -> None:
1✔
450
        self.suffixes = suffixes
×
451
        super().__init__()
×
452

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

456

457
class ColumnNamesNames(ColumnNames):
1✔
458
    """Rename bands with new names."""
459

460
    new_names: list[str]
1✔
461

462
    def __init__(self, new_names: list[str]) -> None:
1✔
463
        self.new_names = new_names
1✔
464
        super().__init__()
1✔
465

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

469

470
class RasterVectorJoin(VectorOperator):
1✔
471
    """A RasterVectorJoin operator."""
472

473
    raster_sources: list[RasterOperator]
1✔
474
    vector_source: VectorOperator
1✔
475
    names: ColumnNames
1✔
476
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1✔
477
    temporal_aggregation_ignore_nodata: bool = False
1✔
478
    feature_aggregation: Literal["first", "mean"] = "mean"
1✔
479
    feature_aggregation_ignore_nodata: bool = False
1✔
480

481
    # pylint: disable=too-many-arguments,too-many-positional-arguments
482
    def __init__(
1✔
483
        self,
484
        raster_sources: list[RasterOperator],
485
        vector_source: VectorOperator,
486
        names: ColumnNames,
487
        temporal_aggregation: Literal["none", "first", "mean"] = "none",
488
        temporal_aggregation_ignore_nodata: bool = False,
489
        feature_aggregation: Literal["first", "mean"] = "mean",
490
        feature_aggregation_ignore_nodata: bool = False,
491
    ):
492
        """Creates a new RasterVectorJoin operator."""
493
        self.raster_source = raster_sources
1✔
494
        self.vector_source = vector_source
1✔
495
        self.names = names
1✔
496
        self.temporal_aggregation = temporal_aggregation
1✔
497
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1✔
498
        self.feature_aggregation = feature_aggregation
1✔
499
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1✔
500

501
    def name(self) -> str:
1✔
502
        return "RasterVectorJoin"
1✔
503

504
    def to_dict(self) -> dict[str, Any]:
1✔
505
        return {
1✔
506
            "type": self.name(),
507
            "params": {
508
                "names": self.names.to_dict(),
509
                "temporalAggregation": self.temporal_aggregation,
510
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
511
                "featureAggregation": self.feature_aggregation,
512
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
513
            },
514
            "sources": {
515
                "vector": self.vector_source.to_dict(),
516
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source],
517
            },
518
        }
519

520
    @classmethod
1✔
521
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterVectorJoin:
1✔
522
        """Returns an operator from a dictionary."""
523
        if operator_dict["type"] != "RasterVectorJoin":
1✔
524
            raise ValueError("Invalid operator type")
×
525

526
        vector_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["vector"]))
1✔
527
        raster_sources = [
1✔
528
            RasterOperator.from_operator_dict(raster_source)
529
            for raster_source in cast(list[dict[str, Any]], operator_dict["sources"]["rasters"])
530
        ]
531

532
        params = operator_dict["params"]
1✔
533
        return RasterVectorJoin(
1✔
534
            raster_sources=raster_sources,
535
            vector_source=vector_source,
536
            names=ColumnNames.from_dict(params["names"]),
537
            temporal_aggregation=cast(Literal["none", "first", "mean"], params["temporalAggregation"]),
538
            temporal_aggregation_ignore_nodata=cast(bool, params["temporalAggregationIgnoreNoData"]),
539
            feature_aggregation=cast(Literal["first", "mean"], params["featureAggregation"]),
540
            feature_aggregation_ignore_nodata=cast(bool, params["featureAggregationIgnoreNoData"]),
541
        )
542

543

544
class PointInPolygonFilter(VectorOperator):
1✔
545
    """A PointInPolygonFilter operator."""
546

547
    point_source: VectorOperator
1✔
548
    polygon_source: VectorOperator
1✔
549

550
    def __init__(
1✔
551
        self,
552
        point_source: VectorOperator,
553
        polygon_source: VectorOperator,
554
    ):
555
        """Creates a new PointInPolygonFilter filter operator."""
556
        self.point_source = point_source
1✔
557
        self.polygon_source = polygon_source
1✔
558

559
    def name(self) -> str:
1✔
560
        return "PointInPolygonFilter"
1✔
561

562
    def to_dict(self) -> dict[str, Any]:
1✔
563
        return {
1✔
564
            "type": self.name(),
565
            "params": {},
566
            "sources": {"points": self.point_source.to_dict(), "polygons": self.polygon_source.to_dict()},
567
        }
568

569
    @classmethod
1✔
570
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> PointInPolygonFilter:
1✔
571
        """Returns an operator from a dictionary."""
572
        if operator_dict["type"] != "PointInPolygonFilter":
1✔
573
            raise ValueError("Invalid operator type")
×
574

575
        point_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["points"]))
1✔
576
        polygon_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["polygons"]))
1✔
577

578
        return PointInPolygonFilter(
1✔
579
            point_source=point_source,
580
            polygon_source=polygon_source,
581
        )
582

583

584
class RasterScaling(RasterOperator):
1✔
585
    """A RasterScaling operator.
586

587
    This operator scales the values of a raster by a given slope and offset.
588

589
    The scaling is done as follows:
590
    y = (x - offset) / slope
591

592
    The unscale mode is the inverse of the scale mode:
593
    x = y * slope + offset
594

595
    """
596

597
    source: RasterOperator
1✔
598
    slope: float | str | None = None
1✔
599
    offset: float | str | None = None
1✔
600
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1✔
601
    output_measurement: str | None = None
1✔
602

603
    def __init__(
1✔
604
        self,
605
        # pylint: disable=too-many-arguments,too-many-positional-arguments
606
        source: RasterOperator,
607
        slope: float | str | None = None,
608
        offset: float | str | None = None,
609
        scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
610
        output_measurement: str | None = None,
611
    ):
612
        """Creates a new RasterScaling operator."""
613
        self.source = source
1✔
614
        self.slope = slope
1✔
615
        self.offset = offset
1✔
616
        self.scaling_mode = scaling_mode
1✔
617
        self.output_measurement = output_measurement
1✔
618
        if output_measurement is not None:
1✔
619
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
620

621
    def name(self) -> str:
1✔
622
        return "RasterScaling"
1✔
623

624
    def to_dict(self) -> dict[str, Any]:
1✔
625
        def offset_scale_dict(key_or_value: float | str | None) -> dict[str, Any]:
1✔
626
            if key_or_value is None:
1✔
627
                return {"type": "auto"}
1✔
628

629
            if isinstance(key_or_value, float):
1✔
630
                return {"type": "constant", "value": key_or_value}
1✔
631

632
            if isinstance(key_or_value, int):
×
633
                return {"type": "constant", "value": float(key_or_value)}
×
634

635
            # TODO: incorporate `domain` field
636
            return {"type": "metadataKey", "key": key_or_value}
×
637

638
        return {
1✔
639
            "type": self.name(),
640
            "params": {
641
                "offset": offset_scale_dict(self.offset),
642
                "slope": offset_scale_dict(self.slope),
643
                "scalingMode": self.scaling_mode,
644
            },
645
            "sources": {"raster": self.source.to_dict()},
646
        }
647

648
    @classmethod
1✔
649
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterScaling:
1✔
650
        if operator_dict["type"] != "RasterScaling":
1✔
651
            raise ValueError("Invalid operator type")
×
652

653
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
654
        params = operator_dict["params"]
1✔
655

656
        def offset_slope_reverse(key_or_value: dict[str, Any] | None) -> float | str | None:
1✔
657
            if key_or_value is None:
1✔
658
                return None
×
659
            if key_or_value["type"] == "constant":
1✔
660
                return key_or_value["value"]
1✔
661
            if key_or_value["type"] == "metadataKey":
1✔
662
                return key_or_value["key"]
×
663
            return None
1✔
664

665
        return RasterScaling(
1✔
666
            source_operator,
667
            slope=offset_slope_reverse(params["slope"]),
668
            offset=offset_slope_reverse(params["offset"]),
669
            scaling_mode=params["scalingMode"],
670
            output_measurement=params.get("outputMeasurement", None),
671
        )
672

673

674
class RasterTypeConversion(RasterOperator):
1✔
675
    """A RasterTypeConversion operator."""
676

677
    source: RasterOperator
1✔
678
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
679

680
    def __init__(
1✔
681
        self,
682
        source: RasterOperator,
683
        output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
684
    ):
685
        """Creates a new RasterTypeConversion operator."""
686
        self.source = source
1✔
687
        self.output_data_type = output_data_type
1✔
688

689
    def name(self) -> str:
1✔
690
        return "RasterTypeConversion"
1✔
691

692
    def to_dict(self) -> dict[str, Any]:
1✔
693
        return {
1✔
694
            "type": self.name(),
695
            "params": {"outputDataType": self.output_data_type},
696
            "sources": {"raster": self.source.to_dict()},
697
        }
698

699
    @classmethod
1✔
700
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterTypeConversion:
1✔
701
        if operator_dict["type"] != "RasterTypeConversion":
1✔
702
            raise ValueError("Invalid operator type")
×
703

704
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
705

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

708

709
class Reprojection(Operator):
1✔
710
    """A Reprojection operator."""
711

712
    source: Operator
1✔
713
    target_spatial_reference: str
1✔
714

715
    def __init__(self, source: Operator, target_spatial_reference: str):
1✔
716
        """Creates a new Reprojection operator."""
717
        self.source = source
1✔
718
        self.target_spatial_reference = target_spatial_reference
1✔
719

720
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
721
        return self.source.data_type()
×
722

723
    def name(self) -> str:
1✔
724
        return "Reprojection"
1✔
725

726
    def to_dict(self) -> dict[str, Any]:
1✔
727
        return {
1✔
728
            "type": self.name(),
729
            "params": {"targetSpatialReference": self.target_spatial_reference},
730
            "sources": {"source": self.source.to_dict()},
731
        }
732

733
    def as_vector(self) -> VectorOperator:
1✔
734
        """Casts this operator to a VectorOperator."""
735
        if self.data_type() != "Vector":
×
736
            raise TypeError("Cannot cast to VectorOperator")
×
737
        return cast(VectorOperator, self)
×
738

739
    def as_raster(self) -> RasterOperator:
1✔
740
        """Casts this operator to a RasterOperator."""
741
        if self.data_type() != "Raster":
×
742
            raise TypeError("Cannot cast to RasterOperator")
×
743
        return cast(RasterOperator, self)
×
744

745
    @classmethod
1✔
746
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Reprojection:
1✔
747
        """Constructs the operator from the given dictionary."""
748
        if operator_dict["type"] != "Reprojection":
1✔
749
            raise ValueError("Invalid operator type")
×
750

751
        source_operator: RasterOperator | VectorOperator
752
        try:
1✔
753
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1✔
754
        except ValueError:
×
755
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
756

757
        return Reprojection(
1✔
758
            source=cast(Operator, source_operator),
759
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"],
760
        )
761

762

763
class Expression(RasterOperator):
1✔
764
    """An Expression operator."""
765

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

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

788
    def name(self) -> str:
1✔
789
        return "Expression"
1✔
790

791
    def to_dict(self) -> dict[str, Any]:
1✔
792
        params = {
1✔
793
            "expression": self.expression,
794
            "outputType": self.output_type,
795
            "mapNoData": self.map_no_data,
796
        }
797
        if self.output_band is not None:
1✔
798
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1✔
799

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

802
    @classmethod
1✔
803
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Expression:
1✔
804
        if operator_dict["type"] != "Expression":
1✔
805
            raise ValueError("Invalid operator type")
×
806

807
        output_band = None
1✔
808
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1✔
809
            raster_band_descriptor = geoengine_api_client.RasterBandDescriptor.from_dict(
1✔
810
                operator_dict["params"]["outputBand"]
811
            )
812
            if raster_band_descriptor is None:
1✔
813
                raise ValueError("Invalid output band")
×
814
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1✔
815

816
        return Expression(
1✔
817
            expression=operator_dict["params"]["expression"],
818
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
819
            output_type=operator_dict["params"]["outputType"],
820
            map_no_data=operator_dict["params"]["mapNoData"],
821
            output_band=output_band,
822
        )
823

824

825
class BandwiseExpression(RasterOperator):
1✔
826
    """A bandwise Expression operator."""
827

828
    expression: str
1✔
829
    source: RasterOperator
1✔
830
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
831
    map_no_data: bool = False
1✔
832

833
    # pylint: disable=too-many-arguments
834
    def __init__(
1✔
835
        self,
836
        expression: str,
837
        source: RasterOperator,
838
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
839
        map_no_data: bool = False,
840
    ):
841
        """Creates a new Expression operator."""
842
        self.expression = expression
×
843
        self.source = source
×
844
        self.output_type = output_type
×
845
        self.map_no_data = map_no_data
×
846

847
    def name(self) -> str:
1✔
848
        return "BandwiseExpression"
×
849

850
    def to_dict(self) -> dict[str, Any]:
1✔
851
        params = {
×
852
            "expression": self.expression,
853
            "outputType": self.output_type,
854
            "mapNoData": self.map_no_data,
855
        }
856

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

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

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

871

872
class GeoVectorDataType(Enum):
1✔
873
    """The output type of geometry vector data."""
874

875
    MULTI_POINT = "MultiPoint"
1✔
876
    MULTI_LINE_STRING = "MultiLineString"
1✔
877
    MULTI_POLYGON = "MultiPolygon"
1✔
878

879

880
class VectorExpression(VectorOperator):
1✔
881
    """The `VectorExpression` operator."""
882

883
    source: VectorOperator
1✔
884

885
    expression: str
1✔
886
    input_columns: list[str]
1✔
887
    output_column: str | GeoVectorDataType
1✔
888
    geometry_column_name = None
1✔
889
    output_measurement: Measurement | None = None
1✔
890

891
    # pylint: disable=too-many-arguments
892
    def __init__(
1✔
893
        self,
894
        source: VectorOperator,
895
        *,
896
        expression: str,
897
        input_columns: list[str],
898
        output_column: str | GeoVectorDataType,
899
        geometry_column_name: str | None = None,
900
        output_measurement: Measurement | None = None,
901
    ):
902
        """Creates a new VectorExpression operator."""
903
        self.source = source
1✔
904

905
        self.expression = expression
1✔
906
        self.input_columns = input_columns
1✔
907
        self.output_column = output_column
1✔
908

909
        self.geometry_column_name = geometry_column_name
1✔
910
        self.output_measurement = output_measurement
1✔
911

912
    def name(self) -> str:
1✔
913
        return "VectorExpression"
1✔
914

915
    def to_dict(self) -> dict[str, Any]:
1✔
916
        output_column_dict = None
1✔
917

918
        if isinstance(self.output_column, dict):
1✔
919
            output_column_dict = self.output_column
1✔
920
        elif isinstance(self.output_column, GeoVectorDataType):
1✔
921
            output_column_dict = {
1✔
922
                "type": "geometry",
923
                "value": self.output_column.value,
924
            }
925
        elif isinstance(self.output_column, str):
1✔
926
            output_column_dict = {
1✔
927
                "type": "column",
928
                "value": self.output_column,
929
            }
930
        else:
931
            raise NotImplementedError("Invalid output column type")
×
932

933
        params = {
1✔
934
            "expression": self.expression,
935
            "inputColumns": self.input_columns,
936
            "outputColumn": output_column_dict,
937
        }  # type: dict[str, Any]
938

939
        if self.geometry_column_name:
1✔
940
            params["geometryColumnName"] = self.geometry_column_name
1✔
941

942
        if self.output_measurement:
1✔
943
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
1✔
944

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

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

952
        geometry_column_name = None
1✔
953
        if "geometryColumnName" in operator_dict["params"]:
1✔
954
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
1✔
955

956
        output_measurement = None
1✔
957
        if "outputMeasurement" in operator_dict["params"]:
1✔
958
            output_measurement = Measurement.from_response(
1✔
959
                geoengine_api_client.Measurement.from_dict(operator_dict["params"]["outputMeasurement"])
960
            )
961

962
        return VectorExpression(
1✔
963
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
964
            expression=operator_dict["params"]["expression"],
965
            input_columns=operator_dict["params"]["inputColumns"],
966
            output_column=operator_dict["params"]["outputColumn"],
967
            geometry_column_name=geometry_column_name,
968
            output_measurement=output_measurement,
969
        )
970

971

972
class TemporalRasterAggregation(RasterOperator):
1✔
973
    """A TemporalRasterAggregation operator."""
974

975
    # pylint: disable=too-many-instance-attributes
976

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

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

1024
    def name(self) -> str:
1✔
1025
        return "TemporalRasterAggregation"
1✔
1026

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

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

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

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

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

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

1073

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

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

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

1097
    def name(self) -> str:
1✔
1098
        return "TimeShift"
1✔
1099

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

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

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

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

1122
    @classmethod
1✔
1123
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TimeShift:
1✔
1124
        """Constructs the operator from the given dictionary."""
1125
        if operator_dict["type"] != "TimeShift":
×
1126
            raise ValueError("Invalid operator type")
×
1127
        source: RasterOperator | VectorOperator
1128
        try:
×
1129
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1130
        except ValueError:
×
1131
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1132

1133
        return TimeShift(
×
1134
            source=source,
1135
            shift_type=operator_dict["params"]["type"],
1136
            granularity=operator_dict["params"]["granularity"],
1137
            value=operator_dict["params"]["value"],
1138
        )
1139

1140

1141
class RenameBands:
1✔
1142
    """Base class for renaming bands of a raster."""
1143

1144
    @abstractmethod
1✔
1145
    def to_dict(self) -> dict[str, Any]:
1✔
1146
        pass
×
1147

1148
    @classmethod
1✔
1149
    def from_dict(cls, rename_dict: dict[str, Any]) -> RenameBands:
1✔
1150
        """Returns a RenameBands object from a dictionary."""
1151
        if rename_dict["type"] == "default":
1✔
1152
            return RenameBandsDefault()
1✔
1153
        if rename_dict["type"] == "suffix":
1✔
1154
            return RenameBandsSuffix(cast(list[str], rename_dict["values"]))
×
1155
        if rename_dict["type"] == "rename":
1✔
1156
            return RenameBandsRename(cast(list[str], rename_dict["values"]))
1✔
1157
        raise ValueError("Invalid rename type")
×
1158

1159
    @classmethod
1✔
1160
    def default(cls) -> RenameBands:
1✔
1161
        return RenameBandsDefault()
×
1162

1163
    @classmethod
1✔
1164
    def suffix(cls, values: list[str]) -> RenameBands:
1✔
1165
        return RenameBandsSuffix(values)
×
1166

1167
    @classmethod
1✔
1168
    def rename(cls, values: list[str]) -> RenameBands:
1✔
1169
        return RenameBandsRename(values)
×
1170

1171

1172
class RenameBandsDefault(RenameBands):
1✔
1173
    """Rename bands with default suffix."""
1174

1175
    def to_dict(self) -> dict[str, Any]:
1✔
1176
        return {"type": "default"}
1✔
1177

1178

1179
class RenameBandsSuffix(RenameBands):
1✔
1180
    """Rename bands with custom suffixes."""
1181

1182
    suffixes: list[str]
1✔
1183

1184
    def __init__(self, suffixes: list[str]) -> None:
1✔
1185
        self.suffixes = suffixes
×
1186
        super().__init__()
×
1187

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

1191

1192
class RenameBandsRename(RenameBands):
1✔
1193
    """Rename bands with new names."""
1194

1195
    new_names: list[str]
1✔
1196

1197
    def __init__(self, new_names: list[str]) -> None:
1✔
1198
        self.new_names = new_names
1✔
1199
        super().__init__()
1✔
1200

1201
    def to_dict(self) -> dict[str, Any]:
1✔
1202
        return {"type": "rename", "values": self.new_names}
1✔
1203

1204

1205
class RasterStacker(RasterOperator):
1✔
1206
    """The RasterStacker operator."""
1207

1208
    sources: list[RasterOperator]
1✔
1209
    rename: RenameBands
1✔
1210

1211
    # pylint: disable=too-many-arguments
1212
    def __init__(self, sources: list[RasterOperator], rename: RenameBands | None = None):
1✔
1213
        """Creates a new RasterStacker operator."""
1214
        if rename is None:
1✔
1215
            rename = RenameBandsDefault()
1✔
1216

1217
        self.sources = sources
1✔
1218
        self.rename = rename
1✔
1219

1220
    def name(self) -> str:
1✔
1221
        return "RasterStacker"
1✔
1222

1223
    def to_dict(self) -> dict[str, Any]:
1✔
1224
        return {
1✔
1225
            "type": self.name(),
1226
            "params": {"renameBands": self.rename.to_dict()},
1227
            "sources": {"rasters": [raster_source.to_dict() for raster_source in self.sources]},
1228
        }
1229

1230
    @classmethod
1✔
1231
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterStacker:
1✔
1232
        if operator_dict["type"] != "RasterStacker":
1✔
1233
            raise ValueError("Invalid operator type")
×
1234

1235
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
1✔
1236
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
1✔
1237

1238
        return RasterStacker(sources=sources, rename=rename)
1✔
1239

1240

1241
class BandNeighborhoodAggregate(RasterOperator):
1✔
1242
    """The BandNeighborhoodAggregate operator."""
1243

1244
    source: RasterOperator
1✔
1245
    aggregate: BandNeighborhoodAggregateParams
1✔
1246

1247
    # pylint: disable=too-many-arguments
1248
    def __init__(self, source: RasterOperator, aggregate: BandNeighborhoodAggregateParams):
1✔
1249
        """Creates a new BandNeighborhoodAggregate operator."""
1250
        self.source = source
×
1251
        self.aggregate = aggregate
×
1252

1253
    def name(self) -> str:
1✔
1254
        return "BandNeighborhoodAggregate"
×
1255

1256
    def to_dict(self) -> dict[str, Any]:
1✔
1257
        return {
×
1258
            "type": self.name(),
1259
            "params": {"aggregate": self.aggregate.to_dict()},
1260
            "sources": {"raster": self.source.to_dict()},
1261
        }
1262

1263
    @classmethod
1✔
1264
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandNeighborhoodAggregate:
1✔
1265
        if operator_dict["type"] != "BandNeighborhoodAggregate":
×
1266
            raise ValueError("Invalid operator type")
×
1267

1268
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1269
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1270

1271
        return BandNeighborhoodAggregate(source=source, aggregate=aggregate)
×
1272

1273

1274
class BandNeighborhoodAggregateParams:
1✔
1275
    """Abstract base class for band neighborhood aggregate params."""
1276

1277
    @abstractmethod
1✔
1278
    def to_dict(self) -> dict[str, Any]:
1✔
1279
        pass
×
1280

1281
    @classmethod
1✔
1282
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1283
        """Returns a BandNeighborhoodAggregate object from a dictionary."""
1284
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
1285
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
1286
        if band_neighborhood_aggregate_dict["type"] == "average":
×
1287
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
1288
        raise ValueError("Invalid neighborhood aggregate type")
×
1289

1290
    @classmethod
1✔
1291
    def first_derivative(cls, equally_spaced_band_distance: float) -> BandNeighborhoodAggregateParams:
1✔
1292
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1293

1294
    @classmethod
1✔
1295
    def average(cls, window_size: int) -> BandNeighborhoodAggregateParams:
1✔
1296
        return BandNeighborhoodAggregateAverage(window_size)
×
1297

1298

1299
@dataclass
1✔
1300
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1✔
1301
    """The first derivative band neighborhood aggregate."""
1302

1303
    equally_spaced_band_distance: float
1✔
1304

1305
    @classmethod
1✔
1306
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1307
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
1308
            raise ValueError("Invalid neighborhood aggregate type")
×
1309

1310
        return BandNeighborhoodAggregateFirstDerivative(band_neighborhood_aggregate_dict["bandDistance"]["distance"])
×
1311

1312
    def to_dict(self) -> dict[str, Any]:
1✔
1313
        return {
×
1314
            "type": "firstDerivative",
1315
            "bandDistance": {"type": "equallySpaced", "distance": self.equally_spaced_band_distance},
1316
        }
1317

1318

1319
@dataclass
1✔
1320
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1✔
1321
    """The average band neighborhood aggregate."""
1322

1323
    window_size: int
1✔
1324

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

1328

1329
class Onnx(RasterOperator):
1✔
1330
    """Onnx ML operator."""
1331

1332
    source: RasterOperator
1✔
1333
    model: str
1✔
1334

1335
    # pylint: disable=too-many-arguments
1336
    def __init__(self, source: RasterOperator, model: str):
1✔
1337
        """Creates a new Onnx operator."""
1338
        self.source = source
1✔
1339
        self.model = model
1✔
1340

1341
    def name(self) -> str:
1✔
1342
        return "Onnx"
1✔
1343

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

1347
    @classmethod
1✔
1348
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Onnx:
1✔
1349
        if operator_dict["type"] != "Onnx":
1✔
1350
            raise ValueError("Invalid operator type")
×
1351

1352
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
1353
        model = operator_dict["params"]["model"]
1✔
1354

1355
        return Onnx(source=source, model=model)
1✔
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