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

geo-engine / geoengine / 38278279550

12 Oct 2026 03:58PM UTC coverage: 87.441% (+0.007%) from 87.434%
38278279550

push

github

web-flow
feat: rename workflow to processing graph in API (#1325)

* docs: add AGENTS.md files for project-specific guidance and update CONTRIBUTING.md and README.md to reference them

* feat: rename workflow to processing graph in API

* backend fixes

* fix: wrong layer type in API

* fix: lints

* fix: add MapService and ViewportSize imports to project.service.ts

* fix: enum destructuring

* fix: rename bands rust

* refactor: rename workflowId to processingGraphId across layers and services

* refactor(python): variable names from 'workflow' to 'processing_graph' across multiple files

* refactor(backend): update return types in project handlers to use specific response types

1586 of 1899 new or added lines in 28 files covered. (83.52%)

3 existing lines in 1 file now uncovered.

132523 of 151557 relevant lines covered (87.44%)

548375.0 hits per line

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

80.11
/python/geoengine/processing_graph_builder/operators.py
1
"""This module contains helpers to create processing graph 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
from typing_extensions import deprecated
1 ✔
14

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

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

20

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

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

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

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

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

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

45
    @deprecated("Use `Operator.to_processing_graph_dict` instead.", category=DeprecationWarning)
1 ✔
46
    def to_workflow_dict(self) -> dict[str, Any]:
1 ✔
47
        """Deprecated: use `Operator.to_processing_graph_dict` instead."""
48
        return self.to_processing_graph_dict()
1 ✔
49

50
    @classmethod
1 ✔
51
    def from_processing_graph_dict(cls, processing_graph) -> Operator:
1 ✔
52
        """Returns an operator from a processing graph dictionary."""
53
        if processing_graph["type"] == "Raster":
1 ✔
54
            return RasterOperator.from_operator_dict(processing_graph["operator"])
1 ✔
55
        if processing_graph["type"] == "Vector":
1 ✔
56
            return VectorOperator.from_operator_dict(processing_graph["operator"])
1 ✔
57

NEW
58
        raise NotImplementedError(f"Unknown processing graph type {processing_graph['type']}")
×
59

60
    @classmethod
1 ✔
61
    @deprecated("Use `Operator.from_processing_graph_dict` instead.", category=DeprecationWarning)
1 ✔
62
    def from_workflow_dict(cls, workflow) -> Operator:
1 ✔
63
        """Deprecated: use `Operator.from_processing_graph_dict` instead."""
64
        return cls.from_processing_graph_dict(workflow)
1 ✔
65

66

67
class RasterOperator(Operator):
1 ✔
68
    """Base class for all raster operators."""
69

70
    @abstractmethod
1 ✔
71
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
72
        pass
×
73

74
    def data_type(self) -> Literal["Raster", "Vector"]:
1 ✔
75
        return "Raster"
1 ✔
76

77
    @classmethod
1 ✔
78
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterOperator:  # pylint: disable=too-many-return-statements
1 ✔
79
        """Returns an operator from a dictionary."""
80
        if operator_dict["type"] == "GdalSource":
1 ✔
81
            return GdalSource.from_operator_dict(operator_dict)
1 ✔
82
        if operator_dict["type"] == "MultiBandGdalSource":
1 ✔
83
            return MultiBandGdalSource.from_operator_dict(operator_dict)
1 ✔
84
        if operator_dict["type"] == "RasterScaling":
1 ✔
85
            return RasterScaling.from_operator_dict(operator_dict)
1 ✔
86
        if operator_dict["type"] == "RasterTypeConversion":
1 ✔
87
            return RasterTypeConversion.from_operator_dict(operator_dict)
1 ✔
88
        if operator_dict["type"] == "Reprojection":
1 ✔
NEW
89
            return Reprojection.from_operator_dict(operator_dict).as_raster()
×
90
        if operator_dict["type"] == "Downsampling":
1 ✔
NEW
91
            return Downsampling.from_operator_dict(operator_dict)
×
92
        if operator_dict["type"] == "Interpolation":
1 ✔
NEW
93
            return Interpolation.from_operator_dict(operator_dict)
×
94
        if operator_dict["type"] == "Expression":
1 ✔
NEW
95
            return Expression.from_operator_dict(operator_dict)
×
96
        if operator_dict["type"] == "BandwiseExpression":
1 ✔
NEW
97
            return BandwiseExpression.from_operator_dict(operator_dict)
×
98
        if operator_dict["type"] == "TimeShift":
1 ✔
NEW
99
            return TimeShift.from_operator_dict(operator_dict).as_raster()
×
100
        if operator_dict["type"] == "TemporalRasterAggregation":
1 ✔
NEW
101
            return TemporalRasterAggregation.from_operator_dict(operator_dict)
×
102
        if operator_dict["type"] == "RasterStacker":
1 ✔
NEW
103
            return RasterStacker.from_operator_dict(operator_dict)
×
104
        if operator_dict["type"] == "BandFilter":
1 ✔
NEW
105
            return BandFilter.from_operator_dict(operator_dict)
×
106
        if operator_dict["type"] == "BandNeighborhoodAggregate":
1 ✔
NEW
107
            return BandNeighborhoodAggregate.from_operator_dict(operator_dict)
×
108
        if operator_dict["type"] == "Onnx":
1 ✔
109
            return Onnx.from_operator_dict(operator_dict)
1 ✔
110

NEW
111
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
112

113

114
class VectorOperator(Operator):
1 ✔
115
    """Base class for all vector operators."""
116

117
    @abstractmethod
1 ✔
118
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
119
        pass
×
120

121
    def data_type(self) -> Literal["Raster", "Vector"]:
1 ✔
122
        return "Vector"
1 ✔
123

124
    @classmethod
1 ✔
125
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> VectorOperator:
1 ✔
126
        """Returns an operator from a dictionary."""
127
        if operator_dict["type"] == "OgrSource":
1 ✔
128
            return OgrSource.from_operator_dict(operator_dict)
1 ✔
129
        if operator_dict["type"] == "Reprojection":
1 ✔
NEW
130
            return Reprojection.from_operator_dict(operator_dict).as_vector()
×
131
        if operator_dict["type"] == "RasterVectorJoin":
1 ✔
NEW
132
            return RasterVectorJoin.from_operator_dict(operator_dict)
×
133
        if operator_dict["type"] == "PointInPolygonFilter":
1 ✔
NEW
134
            return PointInPolygonFilter.from_operator_dict(operator_dict)
×
135
        if operator_dict["type"] == "TimeShift":
1 ✔
NEW
136
            return TimeShift.from_operator_dict(operator_dict).as_vector()
×
137
        if operator_dict["type"] == "VectorExpression":
1 ✔
138
            return VectorExpression.from_operator_dict(operator_dict)
1 ✔
NEW
139
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
140

141

142
class GdalSource(RasterOperator):
1 ✔
143
    """A GDAL source operator."""
144

145
    dataset: str
1 ✔
146

147
    def __init__(self, dataset: str | DatasetName):
1 ✔
148
        """Creates a new GDAL source operator."""
149
        if isinstance(dataset, DatasetName):
1 ✔
150
            dataset = str(dataset)
1 ✔
151
        self.dataset = dataset
1 ✔
152

153
    def name(self) -> str:
1 ✔
154
        return "GdalSource"
1 ✔
155

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

159
    @classmethod
1 ✔
160
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> GdalSource:
1 ✔
161
        """Returns an operator from a dictionary."""
162
        if operator_dict["type"] != "GdalSource":
1 ✔
NEW
163
            raise ValueError("Invalid operator type")
×
164

165
        return GdalSource(cast(str, operator_dict["params"]["data"]))
1 ✔
166

167

168
class MultiBandGdalSource(RasterOperator):
1 ✔
169
    """A multi-band GDAL source operator referencing a named dataset with all its bands."""
170

171
    dataset: str
1 ✔
172

173
    def __init__(self, dataset: str | DatasetName):
1 ✔
174
        """Creates a new multi-band GDAL source operator."""
175
        if isinstance(dataset, DatasetName):
1 ✔
176
            dataset = str(dataset)
1 ✔
177
        self.dataset = dataset
1 ✔
178

179
    def name(self) -> str:
1 ✔
180
        return "MultiBandGdalSource"
1 ✔
181

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

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

191
        return MultiBandGdalSource(cast(str, operator_dict["params"]["data"]))
1 ✔
192

193

194
class OgrSource(VectorOperator):
1 ✔
195
    """An OGR source operator."""
196

197
    dataset: str
1 ✔
198
    attribute_projection: str | None = None
1 ✔
199
    attribute_filters: str | None = None
1 ✔
200

201
    def __init__(
1 ✔
202
        self,
203
        dataset: str | DatasetName,
204
        attribute_projection: str | None = None,
205
        attribute_filters: str | None = None,
206
    ):
207
        """Creates a new OGR source operator."""
208
        if isinstance(dataset, DatasetName):
1 ✔
NEW
209
            dataset = str(dataset)
×
210
        self.dataset = dataset
1 ✔
211
        self.attribute_projection = attribute_projection
1 ✔
212
        self.attribute_filters = attribute_filters
1 ✔
213

214
    def name(self) -> str:
1 ✔
215
        return "OgrSource"
1 ✔
216

217
    def to_dict(self) -> dict[str, Any]:
1 ✔
218
        return {
1 ✔
219
            "type": self.name(),
220
            "params": {
221
                "data": self.dataset,
222
                "attributeProjection": self.attribute_projection,
223
                "attributeFilters": self.attribute_filters,
224
            },
225
        }
226

227
    @classmethod
1 ✔
228
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> OgrSource:
1 ✔
229
        """Returns an operator from a dictionary."""
230
        if operator_dict["type"] != "OgrSource":
1 ✔
NEW
231
            raise ValueError("Invalid operator type")
×
232

233
        params = operator_dict["params"]
1 ✔
234
        return OgrSource(
1 ✔
235
            cast(str, params["data"]),
236
            attribute_projection=cast(str | None, params.get("attributeProjection")),
237
            attribute_filters=cast(str | None, params.get("attributeFilters")),
238
        )
239

240

241
class Interpolation(RasterOperator):
1 ✔
242
    """An interpolation operator."""
243

244
    source: RasterOperator
1 ✔
245
    interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear"
1 ✔
246
    output_method: Literal["resolution", "fraction"] = "resolution"
1 ✔
247
    output_x: float
1 ✔
248
    output_y: float
1 ✔
249
    output_origin_reference: tuple[float, float] | None
1 ✔
250

251
    def __init__(
1 ✔
252
        self,
253
        source_operator: RasterOperator,
254
        output_x: float,
255
        output_y: float,
256
        output_method: Literal["resolution", "fraction"] = "resolution",
257
        interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear",
258
        output_origin_reference: tuple[float, float] | None = None,
259
    ):
260
        # pylint: disable=too-many-arguments,too-many-positional-arguments
261
        """Creates a new interpolation operator."""
262
        self.source = source_operator
1 ✔
263
        self.interpolation = interpolation
1 ✔
264
        self.output_method = output_method
1 ✔
265
        self.output_x = output_x
1 ✔
266
        self.output_y = output_y
1 ✔
267
        self.output_origin_reference = output_origin_reference
1 ✔
268

269
    def name(self) -> str:
1 ✔
270
        return "Interpolation"
1 ✔
271

272
    def to_dict(self) -> dict[str, Any]:
1 ✔
273
        if self.output_method == "fraction":
1 ✔
274
            input_res = {"type": "fraction", "x": self.output_x, "y": self.output_y}
1 ✔
275
        else:
276
            input_res = {"type": "resolution", "x": self.output_x, "y": self.output_y}
1 ✔
277

278
        if self.output_origin_reference is not None:
1 ✔
NEW
279
            return {
×
280
                "type": self.name(),
281
                "params": {
282
                    "interpolation": self.interpolation,
283
                    "outputResolution": input_res,
284
                    "outputOriginReference": {
285
                        "x": self.output_origin_reference[0],
286
                        "y": self.output_origin_reference[1],
287
                    },
288
                },
289
                "sources": {"raster": self.source.to_dict()},
290
            }
291

292
        return {
1 ✔
293
            "type": self.name(),
294
            "params": {
295
                "interpolation": self.interpolation,
296
                "outputResolution": input_res,
297
            },
298
            "sources": {"raster": self.source.to_dict()},
299
        }
300

301
    @classmethod
1 ✔
302
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Interpolation:
1 ✔
303
        """Returns an operator from a dictionary."""
304
        if operator_dict["type"] != "Interpolation":
1 ✔
NEW
305
            raise ValueError("Invalid operator type")
×
306

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

309
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
1 ✔
310
            output_x = float(params["x"])
1 ✔
311
            output_y = float(params["y"])
1 ✔
312

313
            if "type" not in params:
1 ✔
NEW
314
                raise KeyError("Interpolation outputResolution must contain a type: resolution OR fraction")
×
315
            if params["type"] == "fraction":
1 ✔
316
                return ("fraction", output_x, output_y)
1 ✔
NEW
317
            if params["type"] == "resolution":
×
NEW
318
                return ("resolution", output_x, output_y)
×
NEW
319
            raise ValueError(f"Invalid interpolation outputResolution type {params['type']}")
×
320

321
        (output_method, output_x, output_y) = parse_input_params(
1 ✔
322
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
323
        )
324

325
        op = Interpolation(
1 ✔
326
            source_operator=source,
327
            output_method=output_method,
328
            output_x=output_x,
329
            output_y=output_y,
330
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict["params"]["interpolation"]),
331
        )
332

333
        if (
1 ✔
334
            "outputOriginReference" in operator_dict["params"]
335
            and operator_dict["params"]["outputOriginReference"] is not None
336
        ):
NEW
337
            op.output_origin_reference = (
×
338
                float(operator_dict["params"]["outputOriginReference"]["x"]),
339
                float(operator_dict["params"]["outputOriginReference"]["y"]),
340
            )
341

342
        return op
1 ✔
343

344

345
class Downsampling(RasterOperator):
1 ✔
346
    """A Downsampling operator."""
347

348
    source: RasterOperator
1 ✔
349
    sample_method: Literal["nearestNeighbor"] = "nearestNeighbor"
1 ✔
350
    output_method: Literal["resolution", "fraction"] = "resolution"
1 ✔
351
    output_x: float
1 ✔
352
    output_y: float
1 ✔
353
    output_origin_reference: tuple[float, float] | None
1 ✔
354

355
    def __init__(
1 ✔
356
        self,
357
        source_operator: RasterOperator,
358
        output_x: float,
359
        output_y: float,
360
        output_method: Literal["resolution", "fraction"] = "resolution",
361
        sample_method: Literal["nearestNeighbor"] = "nearestNeighbor",
362
        output_origin_reference: tuple[float, float] | None = None,
363
    ):
364
        # pylint: disable=too-many-arguments,too-many-positional-arguments
365
        """Creates a new Downsampling operator."""
366
        self.source = source_operator
1 ✔
367
        self.sample_method = sample_method
1 ✔
368
        self.output_method = output_method
1 ✔
369
        self.output_x = output_x
1 ✔
370
        self.output_y = output_y
1 ✔
371
        self.output_origin_reference = output_origin_reference
1 ✔
372

373
    def name(self) -> str:
1 ✔
374
        return "Downsampling"
1 ✔
375

376
    def to_dict(self) -> dict[str, Any]:
1 ✔
377
        if self.output_method == "fraction":
1 ✔
378
            input_res = {"type": "fraction", "x": self.output_x, "y": self.output_y}
1 ✔
379
        else:
NEW
380
            input_res = {"type": "resolution", "x": self.output_x, "y": self.output_y}
×
381

382
        if self.output_origin_reference is not None:
1 ✔
NEW
383
            return {
×
384
                "type": self.name(),
385
                "params": {
386
                    "samplingMethod": self.sample_method,
387
                    "outputResolution": input_res,
388
                    "outputOriginReference": {
389
                        "x": self.output_origin_reference[0],
390
                        "y": self.output_origin_reference[1],
391
                    },
392
                },
393
                "sources": {"raster": self.source.to_dict()},
394
            }
395

396
        return {
1 ✔
397
            "type": self.name(),
398
            "params": {
399
                "samplingMethod": self.sample_method,
400
                "outputResolution": input_res,
401
            },
402
            "sources": {"raster": self.source.to_dict()},
403
        }
404

405
    @classmethod
1 ✔
406
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Downsampling:
1 ✔
407
        """Returns an operator from a dictionary."""
408
        if operator_dict["type"] != "Downsampling":
1 ✔
NEW
409
            raise ValueError("Invalid operator type")
×
410

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

413
        def parse_input_params(params: dict[str, Any]) -> tuple[Literal["resolution", "fraction"], float, float]:
1 ✔
414
            output_x = float(params["x"])
1 ✔
415
            output_y = float(params["y"])
1 ✔
416

417
            if "type" not in params:
1 ✔
NEW
418
                raise KeyError("Downsampling outputResolution must contain a type: resolution OR fraction")
×
419
            if params["type"] == "fraction":
1 ✔
420
                return ("fraction", output_x, output_y)
1 ✔
NEW
421
            if params["type"] == "resolution":
×
NEW
422
                return ("resolution", output_x, output_y)
×
NEW
423
            raise ValueError(f"Invalid Downsampling outputResolution type {params['type']}")
×
424

425
        (output_method, output_x, output_y) = parse_input_params(
1 ✔
426
            cast(dict[str, Any], operator_dict["params"]["outputResolution"])
427
        )
428

429
        op = Downsampling(
1 ✔
430
            source_operator=source,
431
            output_method=output_method,
432
            output_x=output_x,
433
            output_y=output_y,
434
            sample_method=cast(Literal["nearestNeighbor"], operator_dict["params"]["samplingMethod"]),
435
            output_origin_reference=None,
436
        )
437

438
        if (
1 ✔
439
            "outputOriginReference" in operator_dict["params"]
440
            and operator_dict["params"]["outputOriginReference"] is not None
441
        ):
NEW
442
            op.output_origin_reference = (
×
443
                float(operator_dict["params"]["outputOriginReference"]["x"]),
444
                float(operator_dict["params"]["outputOriginReference"]["y"]),
445
            )
446

447
        return op
1 ✔
448

449

450
class ColumnNames:
1 ✔
451
    """Base class for deriving column names from bands of a raster."""
452

453
    @abstractmethod
1 ✔
454
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
455
        pass
×
456

457
    @classmethod
1 ✔
458
    def from_dict(cls, rename_dict: dict[str, Any]) -> ColumnNames:
1 ✔
459
        """Returns a ColumnNames object from a dictionary."""
460
        if rename_dict["type"] == "default":
1 ✔
NEW
461
            return ColumnNamesDefault()
×
462
        if rename_dict["type"] == "suffix":
1 ✔
NEW
463
            return ColumnNamesSuffix(cast(list[str], rename_dict["values"]))
×
464
        if rename_dict["type"] == "names":
1 ✔
465
            return ColumnNamesNames(cast(list[str], rename_dict["values"]))
1 ✔
NEW
466
        raise ValueError("Invalid rename type")
×
467

468
    @classmethod
1 ✔
469
    def default(cls) -> ColumnNames:
1 ✔
NEW
470
        return ColumnNamesDefault()
×
471

472
    @classmethod
1 ✔
473
    def suffix(cls, values: list[str]) -> ColumnNames:
1 ✔
NEW
474
        return ColumnNamesSuffix(values)
×
475

476
    @classmethod
1 ✔
477
    def rename(cls, values: list[str]) -> ColumnNames:
1 ✔
NEW
478
        return ColumnNamesNames(values)
×
479

480

481
class ColumnNamesDefault(ColumnNames):
1 ✔
482
    """column names with default suffix."""
483

484
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
485
        return {"type": "default"}
×
486

487

488
class ColumnNamesSuffix(ColumnNames):
1 ✔
489
    """Rename bands with custom suffixes."""
490

491
    suffixes: list[str]
1 ✔
492

493
    def __init__(self, suffixes: list[str]) -> None:
1 ✔
NEW
494
        self.suffixes = suffixes
×
NEW
495
        super().__init__()
×
496

497
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
498
        return {"type": "suffix", "values": self.suffixes}
×
499

500

501
class ColumnNamesNames(ColumnNames):
1 ✔
502
    """Rename bands with new names."""
503

504
    new_names: list[str]
1 ✔
505

506
    def __init__(self, new_names: list[str]) -> None:
1 ✔
507
        self.new_names = new_names
1 ✔
508
        super().__init__()
1 ✔
509

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

513

514
class RasterVectorJoin(VectorOperator):
1 ✔
515
    """A RasterVectorJoin operator."""
516

517
    raster_sources: list[RasterOperator]
1 ✔
518
    vector_source: VectorOperator
1 ✔
519
    names: ColumnNames
1 ✔
520
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1 ✔
521
    temporal_aggregation_ignore_nodata: bool = False
1 ✔
522
    feature_aggregation: Literal["first", "mean"] = "mean"
1 ✔
523
    feature_aggregation_ignore_nodata: bool = False
1 ✔
524

525
    # pylint: disable=too-many-arguments,too-many-positional-arguments
526
    def __init__(
1 ✔
527
        self,
528
        raster_sources: list[RasterOperator],
529
        vector_source: VectorOperator,
530
        names: ColumnNames,
531
        temporal_aggregation: Literal["none", "first", "mean"] = "none",
532
        temporal_aggregation_ignore_nodata: bool = False,
533
        feature_aggregation: Literal["first", "mean"] = "mean",
534
        feature_aggregation_ignore_nodata: bool = False,
535
    ):
536
        """Creates a new RasterVectorJoin operator."""
537
        self.raster_source = raster_sources
1 ✔
538
        self.vector_source = vector_source
1 ✔
539
        self.names = names
1 ✔
540
        self.temporal_aggregation = temporal_aggregation
1 ✔
541
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1 ✔
542
        self.feature_aggregation = feature_aggregation
1 ✔
543
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1 ✔
544

545
    def name(self) -> str:
1 ✔
546
        return "RasterVectorJoin"
1 ✔
547

548
    def to_dict(self) -> dict[str, Any]:
1 ✔
549
        return {
1 ✔
550
            "type": self.name(),
551
            "params": {
552
                "names": self.names.to_dict(),
553
                "temporalAggregation": self.temporal_aggregation,
554
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
555
                "featureAggregation": self.feature_aggregation,
556
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
557
            },
558
            "sources": {
559
                "vector": self.vector_source.to_dict(),
560
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source],
561
            },
562
        }
563

564
    @classmethod
1 ✔
565
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterVectorJoin:
1 ✔
566
        """Returns an operator from a dictionary."""
567
        if operator_dict["type"] != "RasterVectorJoin":
1 ✔
NEW
568
            raise ValueError("Invalid operator type")
×
569

570
        vector_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["vector"]))
1 ✔
571
        raster_sources = [
1 ✔
572
            RasterOperator.from_operator_dict(raster_source)
573
            for raster_source in cast(list[dict[str, Any]], operator_dict["sources"]["rasters"])
574
        ]
575

576
        params = operator_dict["params"]
1 ✔
577
        return RasterVectorJoin(
1 ✔
578
            raster_sources=raster_sources,
579
            vector_source=vector_source,
580
            names=ColumnNames.from_dict(params["names"]),
581
            temporal_aggregation=cast(Literal["none", "first", "mean"], params["temporalAggregation"]),
582
            temporal_aggregation_ignore_nodata=cast(bool, params["temporalAggregationIgnoreNoData"]),
583
            feature_aggregation=cast(Literal["first", "mean"], params["featureAggregation"]),
584
            feature_aggregation_ignore_nodata=cast(bool, params["featureAggregationIgnoreNoData"]),
585
        )
586

587

588
class PointInPolygonFilter(VectorOperator):
1 ✔
589
    """A PointInPolygonFilter operator."""
590

591
    point_source: VectorOperator
1 ✔
592
    polygon_source: VectorOperator
1 ✔
593

594
    def __init__(
1 ✔
595
        self,
596
        point_source: VectorOperator,
597
        polygon_source: VectorOperator,
598
    ):
599
        """Creates a new PointInPolygonFilter filter operator."""
600
        self.point_source = point_source
1 ✔
601
        self.polygon_source = polygon_source
1 ✔
602

603
    def name(self) -> str:
1 ✔
604
        return "PointInPolygonFilter"
1 ✔
605

606
    def to_dict(self) -> dict[str, Any]:
1 ✔
607
        return {
1 ✔
608
            "type": self.name(),
609
            "params": {},
610
            "sources": {"points": self.point_source.to_dict(), "polygons": self.polygon_source.to_dict()},
611
        }
612

613
    @classmethod
1 ✔
614
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> PointInPolygonFilter:
1 ✔
615
        """Returns an operator from a dictionary."""
616
        if operator_dict["type"] != "PointInPolygonFilter":
1 ✔
NEW
617
            raise ValueError("Invalid operator type")
×
618

619
        point_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["points"]))
1 ✔
620
        polygon_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["polygons"]))
1 ✔
621

622
        return PointInPolygonFilter(
1 ✔
623
            point_source=point_source,
624
            polygon_source=polygon_source,
625
        )
626

627

628
class RasterScaling(RasterOperator):
1 ✔
629
    """A RasterScaling operator.
630

631
    This operator scales the values of a raster by a given slope and offset.
632

633
    The scaling is done as follows:
634
    y = (x - offset) / slope
635

636
    The unscale mode is the inverse of the scale mode:
637
    x = y * slope + offset
638

639
    """
640

641
    source: RasterOperator
1 ✔
642
    slope: float | str | None = None
1 ✔
643
    offset: float | str | None = None
1 ✔
644
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1 ✔
645
    output_measurement: str | None = None
1 ✔
646

647
    def __init__(
1 ✔
648
        self,
649
        # pylint: disable=too-many-arguments,too-many-positional-arguments
650
        source: RasterOperator,
651
        slope: float | str | None = None,
652
        offset: float | str | None = None,
653
        scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
654
        output_measurement: str | None = None,
655
    ):
656
        """Creates a new RasterScaling operator."""
657
        self.source = source
1 ✔
658
        self.slope = slope
1 ✔
659
        self.offset = offset
1 ✔
660
        self.scaling_mode = scaling_mode
1 ✔
661
        self.output_measurement = output_measurement
1 ✔
662
        if output_measurement is not None:
1 ✔
NEW
663
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
664

665
    def name(self) -> str:
1 ✔
666
        return "RasterScaling"
1 ✔
667

668
    def to_dict(self) -> dict[str, Any]:
1 ✔
669
        def offset_scale_dict(key_or_value: float | str | None) -> dict[str, Any]:
1 ✔
670
            if key_or_value is None:
1 ✔
671
                return {"type": "auto"}
1 ✔
672

673
            if isinstance(key_or_value, float):
1 ✔
674
                return {"type": "constant", "value": key_or_value}
1 ✔
675

676
            if isinstance(key_or_value, int):
1 ✔
NEW
677
                return {"type": "constant", "value": float(key_or_value)}
×
678

679
            # TODO: incorporate `domain` field
680
            return {"type": "metadataKey", "value": {"key": key_or_value}}
1 ✔
681

682
        return {
1 ✔
683
            "type": self.name(),
684
            "params": {
685
                "offset": offset_scale_dict(self.offset),
686
                "slope": offset_scale_dict(self.slope),
687
                "scalingMode": self.scaling_mode,
688
            },
689
            "sources": {"raster": self.source.to_dict()},
690
        }
691

692
    @classmethod
1 ✔
693
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterScaling:
1 ✔
694
        if operator_dict["type"] != "RasterScaling":
1 ✔
NEW
695
            raise ValueError("Invalid operator type")
×
696

697
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1 ✔
698
        params = operator_dict["params"]
1 ✔
699

700
        def offset_slope_reverse(key_or_value: dict[str, Any] | None) -> float | str | None:
1 ✔
701
            if key_or_value is None:
1 ✔
NEW
702
                return None
×
703
            if key_or_value["type"] == "constant":
1 ✔
704
                return key_or_value["value"]
1 ✔
705
            if key_or_value["type"] == "metadataKey":
1 ✔
706
                return key_or_value["value"]["key"]
1 ✔
707
            return None
1 ✔
708

709
        return RasterScaling(
1 ✔
710
            source_operator,
711
            slope=offset_slope_reverse(params["slope"]),
712
            offset=offset_slope_reverse(params["offset"]),
713
            scaling_mode=params["scalingMode"],
714
            output_measurement=params.get("outputMeasurement", None),
715
        )
716

717

718
class RasterTypeConversion(RasterOperator):
1 ✔
719
    """A RasterTypeConversion operator."""
720

721
    source: RasterOperator
1 ✔
722
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1 ✔
723

724
    def __init__(
1 ✔
725
        self,
726
        source: RasterOperator,
727
        output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
728
    ):
729
        """Creates a new RasterTypeConversion operator."""
730
        self.source = source
1 ✔
731
        self.output_data_type = output_data_type
1 ✔
732

733
    def name(self) -> str:
1 ✔
734
        return "RasterTypeConversion"
1 ✔
735

736
    def to_dict(self) -> dict[str, Any]:
1 ✔
737
        return {
1 ✔
738
            "type": self.name(),
739
            "params": {"outputDataType": self.output_data_type},
740
            "sources": {"raster": self.source.to_dict()},
741
        }
742

743
    @classmethod
1 ✔
744
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterTypeConversion:
1 ✔
745
        if operator_dict["type"] != "RasterTypeConversion":
1 ✔
NEW
746
            raise ValueError("Invalid operator type")
×
747

748
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1 ✔
749

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

752

753
class Reprojection(Operator):
1 ✔
754
    """A Reprojection operator."""
755

756
    source: Operator
1 ✔
757
    target_spatial_reference: str
1 ✔
758

759
    def __init__(self, source: Operator, target_spatial_reference: str):
1 ✔
760
        """Creates a new Reprojection operator."""
761
        self.source = source
1 ✔
762
        self.target_spatial_reference = target_spatial_reference
1 ✔
763

764
    def data_type(self) -> Literal["Raster", "Vector"]:
1 ✔
NEW
765
        return self.source.data_type()
×
766

767
    def name(self) -> str:
1 ✔
768
        return "Reprojection"
1 ✔
769

770
    def to_dict(self) -> dict[str, Any]:
1 ✔
771
        return {
1 ✔
772
            "type": self.name(),
773
            "params": {"targetSpatialReference": self.target_spatial_reference},
774
            "sources": {"source": self.source.to_dict()},
775
        }
776

777
    def as_vector(self) -> VectorOperator:
1 ✔
778
        """Casts this operator to a VectorOperator."""
NEW
779
        if self.data_type() != "Vector":
×
NEW
780
            raise TypeError("Cannot cast to VectorOperator")
×
NEW
781
        return cast(VectorOperator, self)
×
782

783
    def as_raster(self) -> RasterOperator:
1 ✔
784
        """Casts this operator to a RasterOperator."""
NEW
785
        if self.data_type() != "Raster":
×
NEW
786
            raise TypeError("Cannot cast to RasterOperator")
×
NEW
787
        return cast(RasterOperator, self)
×
788

789
    @classmethod
1 ✔
790
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Reprojection:
1 ✔
791
        """Constructs the operator from the given dictionary."""
792
        if operator_dict["type"] != "Reprojection":
1 ✔
NEW
793
            raise ValueError("Invalid operator type")
×
794

795
        source_operator: RasterOperator | VectorOperator
796
        try:
1 ✔
797
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1 ✔
NEW
798
        except ValueError:
×
NEW
799
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
800

801
        return Reprojection(
1 ✔
802
            source=cast(Operator, source_operator),
803
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"],
804
        )
805

806

807
class Expression(RasterOperator):
1 ✔
808
    """An Expression operator."""
809

810
    expression: str
1 ✔
811
    source: RasterOperator
1 ✔
812
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1 ✔
813
    map_no_data: bool = False
1 ✔
814
    output_band: RasterBandDescriptor | None = None
1 ✔
815

816
    # pylint: disable=too-many-arguments,too-many-positional-arguments
817
    def __init__(
1 ✔
818
        self,
819
        expression: str,
820
        source: RasterOperator,
821
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
822
        map_no_data: bool = False,
823
        output_band: RasterBandDescriptor | None = None,
824
    ):
825
        """Creates a new Expression operator."""
826
        self.expression = expression
1 ✔
827
        self.source = source
1 ✔
828
        self.output_type = output_type
1 ✔
829
        self.map_no_data = map_no_data
1 ✔
830
        self.output_band = output_band
1 ✔
831

832
    def name(self) -> str:
1 ✔
833
        return "Expression"
1 ✔
834

835
    def to_dict(self) -> dict[str, Any]:
1 ✔
836
        params = {
1 ✔
837
            "expression": self.expression,
838
            "outputType": self.output_type,
839
            "mapNoData": self.map_no_data,
840
        }
841
        if self.output_band is not None:
1 ✔
842
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1 ✔
843

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

846
    @classmethod
1 ✔
847
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Expression:
1 ✔
848
        if operator_dict["type"] != "Expression":
1 ✔
NEW
849
            raise ValueError("Invalid operator type")
×
850

851
        output_band = None
1 ✔
852
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1 ✔
853
            raster_band_descriptor = geoengine_api_client.RasterBandDescriptor.from_dict(
1 ✔
854
                operator_dict["params"]["outputBand"]
855
            )
856
            if raster_band_descriptor is None:
1 ✔
NEW
857
                raise ValueError("Invalid output band")
×
858
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1 ✔
859

860
        return Expression(
1 ✔
861
            expression=operator_dict["params"]["expression"],
862
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
863
            output_type=operator_dict["params"]["outputType"],
864
            map_no_data=operator_dict["params"]["mapNoData"],
865
            output_band=output_band,
866
        )
867

868

869
class BandwiseExpression(RasterOperator):
1 ✔
870
    """A bandwise Expression operator."""
871

872
    expression: str
1 ✔
873
    source: RasterOperator
1 ✔
874
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1 ✔
875
    map_no_data: bool = False
1 ✔
876

877
    # pylint: disable=too-many-arguments
878
    def __init__(
1 ✔
879
        self,
880
        expression: str,
881
        source: RasterOperator,
882
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
883
        map_no_data: bool = False,
884
    ):
885
        """Creates a new Expression operator."""
NEW
886
        self.expression = expression
×
NEW
887
        self.source = source
×
NEW
888
        self.output_type = output_type
×
NEW
889
        self.map_no_data = map_no_data
×
890

891
    def name(self) -> str:
1 ✔
NEW
892
        return "BandwiseExpression"
×
893

894
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
895
        params = {
×
896
            "expression": self.expression,
897
            "outputType": self.output_type,
898
            "mapNoData": self.map_no_data,
899
        }
900

NEW
901
        return {"type": self.name(), "params": params, "sources": {"raster": self.source.to_dict()}}
×
902

903
    @classmethod
1 ✔
904
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandwiseExpression:
1 ✔
NEW
905
        if operator_dict["type"] != "BandwiseExpression":
×
NEW
906
            raise ValueError("Invalid operator type")
×
907

NEW
908
        return BandwiseExpression(
×
909
            expression=operator_dict["params"]["expression"],
910
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
911
            output_type=operator_dict["params"]["outputType"],
912
            map_no_data=operator_dict["params"]["mapNoData"],
913
        )
914

915

916
class GeoVectorDataType(Enum):
1 ✔
917
    """The output type of geometry vector data."""
918

919
    MULTI_POINT = "MultiPoint"
1 ✔
920
    MULTI_LINE_STRING = "MultiLineString"
1 ✔
921
    MULTI_POLYGON = "MultiPolygon"
1 ✔
922

923

924
class VectorExpression(VectorOperator):
1 ✔
925
    """The `VectorExpression` operator."""
926

927
    source: VectorOperator
1 ✔
928

929
    expression: str
1 ✔
930
    input_columns: list[str]
1 ✔
931
    output_column: str | GeoVectorDataType
1 ✔
932
    geometry_column_name = None
1 ✔
933
    output_measurement: Measurement | None = None
1 ✔
934

935
    # pylint: disable=too-many-arguments
936
    def __init__(
1 ✔
937
        self,
938
        source: VectorOperator,
939
        *,
940
        expression: str,
941
        input_columns: list[str],
942
        output_column: str | GeoVectorDataType,
943
        geometry_column_name: str | None = None,
944
        output_measurement: Measurement | None = None,
945
    ):
946
        """Creates a new VectorExpression operator."""
947
        self.source = source
1 ✔
948

949
        self.expression = expression
1 ✔
950
        self.input_columns = input_columns
1 ✔
951
        self.output_column = output_column
1 ✔
952

953
        self.geometry_column_name = geometry_column_name
1 ✔
954
        self.output_measurement = output_measurement
1 ✔
955

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

959
    def to_dict(self) -> dict[str, Any]:
1 ✔
960
        output_column_dict = None
1 ✔
961

962
        if isinstance(self.output_column, dict):
1 ✔
963
            output_column_dict = self.output_column
1 ✔
964
        elif isinstance(self.output_column, GeoVectorDataType):
1 ✔
965
            output_column_dict = {
1 ✔
966
                "type": "geometry",
967
                "value": self.output_column.value,
968
            }
969
        elif isinstance(self.output_column, str):
1 ✔
970
            output_column_dict = {
1 ✔
971
                "type": "column",
972
                "value": self.output_column,
973
            }
974
        else:
NEW
975
            raise NotImplementedError("Invalid output column type")
×
976

977
        params = {
1 ✔
978
            "expression": self.expression,
979
            "inputColumns": self.input_columns,
980
            "outputColumn": output_column_dict,
981
        }  # type: dict[str, Any]
982

983
        if self.geometry_column_name:
1 ✔
984
            params["geometryColumnName"] = self.geometry_column_name
1 ✔
985

986
        if self.output_measurement:
1 ✔
987
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
1 ✔
988

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

991
    @classmethod
1 ✔
992
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> VectorExpression:
1 ✔
993
        if operator_dict["type"] != "VectorExpression":
1 ✔
NEW
994
            raise ValueError("Invalid operator type")
×
995

996
        geometry_column_name = None
1 ✔
997
        if "geometryColumnName" in operator_dict["params"]:
1 ✔
998
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
1 ✔
999

1000
        output_measurement = None
1 ✔
1001
        if "outputMeasurement" in operator_dict["params"]:
1 ✔
1002
            output_measurement = Measurement.from_response(
1 ✔
1003
                geoengine_api_client.Measurement.from_dict(operator_dict["params"]["outputMeasurement"])
1004
            )
1005

1006
        return VectorExpression(
1 ✔
1007
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
1008
            expression=operator_dict["params"]["expression"],
1009
            input_columns=operator_dict["params"]["inputColumns"],
1010
            output_column=operator_dict["params"]["outputColumn"],
1011
            geometry_column_name=geometry_column_name,
1012
            output_measurement=output_measurement,
1013
        )
1014

1015

1016
class TemporalRasterAggregation(RasterOperator):
1 ✔
1017
    """A TemporalRasterAggregation operator."""
1018

1019
    # pylint: disable=too-many-instance-attributes
1020

1021
    source: RasterOperator
1 ✔
1022
    aggregation_type: Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"]
1 ✔
1023
    ignore_no_data: bool = False
1 ✔
1024
    window_granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days"
1 ✔
1025
    window_size: int = 1
1 ✔
1026
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None
1 ✔
1027
    percentile: float | None = None
1 ✔
1028
    window_ref: np.datetime64 | None = None
1 ✔
1029

1030
    # pylint: disable=too-many-arguments,too-many-positional-arguments
1031
    def __init__(
1 ✔
1032
        self,
1033
        source: RasterOperator,
1034
        aggregation_type: Literal[
1035
            "mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"
1036
        ],
1037
        ignore_no_data: bool = False,
1038
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days",
1039
        window_size: int = 1,
1040
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None,
1041
        percentile: float | None = None,
1042
        window_reference: datetime.datetime | np.datetime64 | None = None,
1043
    ):
1044
        """Creates a new TemporalRasterAggregation operator."""
1045
        self.source = source
1 ✔
1046
        self.aggregation_type = aggregation_type
1 ✔
1047
        self.ignore_no_data = ignore_no_data
1 ✔
1048
        self.window_granularity = granularity
1 ✔
1049
        self.window_size = window_size
1 ✔
1050
        self.output_type = output_type
1 ✔
1051
        if self.aggregation_type == "percentileEstimate":
1 ✔
NEW
1052
            if percentile is None:
×
NEW
1053
                raise ValueError("Percentile must be set for percentileEstimate")
×
NEW
1054
            if percentile <= 0.0 or percentile > 1.0:
×
NEW
1055
                raise ValueError("Percentile must be > 0.0 and <= 1.0")
×
NEW
1056
            self.percentile = percentile
×
1057
        if window_reference is not None:
1 ✔
NEW
1058
            if isinstance(window_reference, np.datetime64):
×
NEW
1059
                self.window_ref = window_reference
×
NEW
1060
            elif isinstance(window_reference, datetime.datetime):
×
1061
                # We assume that a datetime without a timezone means UTC
NEW
1062
                if window_reference.tzinfo is not None:
×
NEW
1063
                    window_reference = window_reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
×
NEW
1064
                self.window_ref = np.datetime64(window_reference)
×
1065
            else:
NEW
1066
                raise ValueError("`window_reference` must be of type `datetime.datetime` or `numpy.datetime64`")
×
1067

1068
    def name(self) -> str:
1 ✔
1069
        return "TemporalRasterAggregation"
1 ✔
1070

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

1074
        return {
1 ✔
1075
            "type": self.name(),
1076
            "params": {
1077
                "aggregation": {
1078
                    "type": self.aggregation_type,
1079
                    "ignoreNoData": self.ignore_no_data,
1080
                    "percentile": self.percentile,
1081
                },
1082
                "window": {"granularity": self.window_granularity, "step": self.window_size},
1083
                "windowReference": w_ref,
1084
                "outputType": self.output_type,
1085
            },
1086
            "sources": {"raster": self.source.to_dict()},
1087
        }
1088

1089
    @classmethod
1 ✔
1090
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TemporalRasterAggregation:
1 ✔
1091
        if operator_dict["type"] != "TemporalRasterAggregation":
1 ✔
NEW
1092
            raise ValueError("Invalid operator type")
×
1093

1094
        w_ref: datetime.datetime | np.datetime64 | None = None
1 ✔
1095
        if "windowReference" in operator_dict["params"]:
1 ✔
1096
            t_ref = operator_dict["params"]["windowReference"]
1 ✔
1097
            if isinstance(t_ref, str):
1 ✔
NEW
1098
                w_ref = datetime.datetime.fromisoformat(t_ref)
×
1099
            if isinstance(t_ref, int):
1 ✔
NEW
1100
                w_ref = np.datetime64(t_ref, "ms")
×
1101

1102
        percentile = None
1 ✔
1103
        if "percentile" in operator_dict["params"]["aggregation"]:
1 ✔
1104
            percentile = operator_dict["params"]["aggregation"]["percentile"]
1 ✔
1105

1106
        return TemporalRasterAggregation(
1 ✔
1107
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
1108
            aggregation_type=operator_dict["params"]["aggregation"]["type"],
1109
            ignore_no_data=operator_dict["params"]["aggregation"]["ignoreNoData"],
1110
            granularity=operator_dict["params"]["window"]["granularity"],
1111
            window_size=operator_dict["params"]["window"]["step"],
1112
            output_type=operator_dict["params"]["outputType"],
1113
            window_reference=w_ref,
1114
            percentile=percentile,
1115
        )
1116

1117

1118
class TimeShift(Operator):
1 ✔
1119
    """A RasterTypeConversion operator."""
1120

1121
    source: RasterOperator | VectorOperator
1 ✔
1122
    shift_type: Literal["relative", "absolute"]
1 ✔
1123
    granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"]
1 ✔
1124
    value: int
1 ✔
1125

1126
    def __init__(
1 ✔
1127
        self,
1128
        source: RasterOperator | VectorOperator,
1129
        shift_type: Literal["relative", "absolute"],
1130
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"],
1131
        value: int,
1132
    ):
1133
        """Creates a new RasterTypeConversion operator."""
1134
        if shift_type == "absolute":
1 ✔
NEW
1135
            raise NotImplementedError("Absolute time shifts are not supported yet")
×
1136
        self.source = source
1 ✔
1137
        self.shift_type = shift_type
1 ✔
1138
        self.granularity = granularity
1 ✔
1139
        self.value = value
1 ✔
1140

1141
    def name(self) -> str:
1 ✔
1142
        return "TimeShift"
1 ✔
1143

1144
    def data_type(self) -> Literal["Vector", "Raster"]:
1 ✔
NEW
1145
        return self.source.data_type()
×
1146

1147
    def as_vector(self) -> VectorOperator:
1 ✔
1148
        """Casts this operator to a VectorOperator."""
NEW
1149
        if self.data_type() != "Vector":
×
NEW
1150
            raise TypeError("Cannot cast to VectorOperator")
×
NEW
1151
        return cast(VectorOperator, self)
×
1152

1153
    def as_raster(self) -> RasterOperator:
1 ✔
1154
        """Casts this operator to a RasterOperator."""
NEW
1155
        if self.data_type() != "Raster":
×
NEW
1156
            raise TypeError("Cannot cast to RasterOperator")
×
NEW
1157
        return cast(RasterOperator, self)
×
1158

1159
    def to_dict(self) -> dict[str, Any]:
1 ✔
1160
        return {
1 ✔
1161
            "type": self.name(),
1162
            "params": {"type": self.shift_type, "granularity": self.granularity, "value": self.value},
1163
            "sources": {"source": self.source.to_dict()},
1164
        }
1165

1166
    @classmethod
1 ✔
1167
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TimeShift:
1 ✔
1168
        """Constructs the operator from the given dictionary."""
NEW
1169
        if operator_dict["type"] != "TimeShift":
×
NEW
1170
            raise ValueError("Invalid operator type")
×
1171
        source: RasterOperator | VectorOperator
NEW
1172
        try:
×
NEW
1173
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
NEW
1174
        except ValueError:
×
NEW
1175
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
1176

NEW
1177
        return TimeShift(
×
1178
            source=source,
1179
            shift_type=operator_dict["params"]["type"],
1180
            granularity=operator_dict["params"]["granularity"],
1181
            value=operator_dict["params"]["value"],
1182
        )
1183

1184

1185
class RenameBands:
1 ✔
1186
    """Base class for renaming bands of a raster."""
1187

1188
    @abstractmethod
1 ✔
1189
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1190
        pass
×
1191

1192
    @classmethod
1 ✔
1193
    def from_dict(cls, rename_dict: dict[str, Any]) -> RenameBands:
1 ✔
1194
        """Returns a RenameBands object from a dictionary."""
1195
        if rename_dict["type"] == "default":
1 ✔
1196
            return RenameBandsDefault()
1 ✔
1197
        if rename_dict["type"] == "suffix":
1 ✔
NEW
1198
            return RenameBandsSuffix(cast(list[str], rename_dict["values"]))
×
1199
        if rename_dict["type"] == "rename":
1 ✔
1200
            return RenameBandsRename(cast(list[str], rename_dict["values"]))
1 ✔
NEW
1201
        raise ValueError("Invalid rename type")
×
1202

1203
    @classmethod
1 ✔
1204
    def default(cls) -> RenameBands:
1 ✔
NEW
1205
        return RenameBandsDefault()
×
1206

1207
    @classmethod
1 ✔
1208
    def suffix(cls, values: list[str]) -> RenameBands:
1 ✔
NEW
1209
        return RenameBandsSuffix(values)
×
1210

1211
    @classmethod
1 ✔
1212
    def rename(cls, values: list[str]) -> RenameBands:
1 ✔
NEW
1213
        return RenameBandsRename(values)
×
1214

1215

1216
class RenameBandsDefault(RenameBands):
1 ✔
1217
    """Rename bands with default suffix."""
1218

1219
    def to_dict(self) -> dict[str, Any]:
1 ✔
1220
        return {"type": "default"}
1 ✔
1221

1222

1223
class RenameBandsSuffix(RenameBands):
1 ✔
1224
    """Rename bands with custom suffixes."""
1225

1226
    suffixes: list[str]
1 ✔
1227

1228
    def __init__(self, suffixes: list[str]) -> None:
1 ✔
NEW
1229
        self.suffixes = suffixes
×
NEW
1230
        super().__init__()
×
1231

1232
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1233
        return {"type": "suffix", "values": self.suffixes}
×
1234

1235

1236
class RenameBandsRename(RenameBands):
1 ✔
1237
    """Rename bands with new names."""
1238

1239
    new_names: list[str]
1 ✔
1240

1241
    def __init__(self, new_names: list[str]) -> None:
1 ✔
1242
        self.new_names = new_names
1 ✔
1243
        super().__init__()
1 ✔
1244

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

1248

1249
class RasterStacker(RasterOperator):
1 ✔
1250
    """The RasterStacker operator."""
1251

1252
    sources: list[RasterOperator]
1 ✔
1253
    rename: RenameBands
1 ✔
1254

1255
    # pylint: disable=too-many-arguments
1256
    def __init__(self, sources: list[RasterOperator], rename: RenameBands | None = None):
1 ✔
1257
        """Creates a new RasterStacker operator."""
1258
        if rename is None:
1 ✔
1259
            rename = RenameBandsDefault()
1 ✔
1260

1261
        self.sources = sources
1 ✔
1262
        self.rename = rename
1 ✔
1263

1264
    def name(self) -> str:
1 ✔
1265
        return "RasterStacker"
1 ✔
1266

1267
    def to_dict(self) -> dict[str, Any]:
1 ✔
1268
        return {
1 ✔
1269
            "type": self.name(),
1270
            "params": {"renameBands": self.rename.to_dict()},
1271
            "sources": {"rasters": [raster_source.to_dict() for raster_source in self.sources]},
1272
        }
1273

1274
    @classmethod
1 ✔
1275
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterStacker:
1 ✔
1276
        if operator_dict["type"] != "RasterStacker":
1 ✔
NEW
1277
            raise ValueError("Invalid operator type")
×
1278

1279
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
1 ✔
1280
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
1 ✔
1281

1282
        return RasterStacker(sources=sources, rename=rename)
1 ✔
1283

1284

1285
class BandFilter(RasterOperator):
1 ✔
1286
    """The BandFilter operator.
1287

1288
    Selects a subset of the source's bands, either by band names or by band
1289
    indices. The order of the remaining bands is preserved and the output
1290
    bands are renumbered to 0..n.
1291
    """
1292

1293
    source: RasterOperator
1 ✔
1294
    bands: list[str] | list[int]
1 ✔
1295

1296
    def __init__(self, source: RasterOperator, bands: list[str] | list[int]):
1 ✔
1297
        """Creates a new BandFilter operator."""
1298
        if not bands:
1 ✔
1299
            raise ValueError("Must select at least one band")
1 ✔
1300

1301
        self.source = source
1 ✔
1302
        self.bands = bands
1 ✔
1303

1304
    def name(self) -> str:
1 ✔
1305
        return "BandFilter"
1 ✔
1306

1307
    def to_dict(self) -> dict[str, Any]:
1 ✔
1308
        return {
1 ✔
1309
            "type": self.name(),
1310
            "params": {"bands": self.bands},
1311
            "sources": {"raster": self.source.to_dict()},
1312
        }
1313

1314
    @classmethod
1 ✔
1315
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandFilter:
1 ✔
1316
        """Returns an operator from a dictionary."""
1317
        if operator_dict["type"] != "BandFilter":
1 ✔
NEW
1318
            raise ValueError("Invalid operator type")
×
1319

1320
        source = RasterOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["raster"]))
1 ✔
1321
        bands = cast(list[str] | list[int], operator_dict["params"]["bands"])
1 ✔
1322

1323
        return BandFilter(source=source, bands=bands)
1 ✔
1324

1325

1326
class BandNeighborhoodAggregate(RasterOperator):
1 ✔
1327
    """The BandNeighborhoodAggregate operator."""
1328

1329
    source: RasterOperator
1 ✔
1330
    aggregate: BandNeighborhoodAggregateParams
1 ✔
1331

1332
    # pylint: disable=too-many-arguments
1333
    def __init__(self, source: RasterOperator, aggregate: BandNeighborhoodAggregateParams):
1 ✔
1334
        """Creates a new BandNeighborhoodAggregate operator."""
NEW
1335
        self.source = source
×
NEW
1336
        self.aggregate = aggregate
×
1337

1338
    def name(self) -> str:
1 ✔
NEW
1339
        return "BandNeighborhoodAggregate"
×
1340

1341
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1342
        return {
×
1343
            "type": self.name(),
1344
            "params": {"aggregate": self.aggregate.to_dict()},
1345
            "sources": {"raster": self.source.to_dict()},
1346
        }
1347

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

NEW
1353
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
NEW
1354
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1355

NEW
1356
        return BandNeighborhoodAggregate(source=source, aggregate=aggregate)
×
1357

1358

1359
class BandNeighborhoodAggregateParams:
1 ✔
1360
    """Abstract base class for band neighborhood aggregate params."""
1361

1362
    @abstractmethod
1 ✔
1363
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1364
        pass
×
1365

1366
    @classmethod
1 ✔
1367
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1 ✔
1368
        """Returns a BandNeighborhoodAggregate object from a dictionary."""
NEW
1369
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
NEW
1370
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
NEW
1371
        if band_neighborhood_aggregate_dict["type"] == "average":
×
NEW
1372
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
NEW
1373
        raise ValueError("Invalid neighborhood aggregate type")
×
1374

1375
    @classmethod
1 ✔
1376
    def first_derivative(cls, equally_spaced_band_distance: float) -> BandNeighborhoodAggregateParams:
1 ✔
NEW
1377
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1378

1379
    @classmethod
1 ✔
1380
    def average(cls, window_size: int) -> BandNeighborhoodAggregateParams:
1 ✔
NEW
1381
        return BandNeighborhoodAggregateAverage(window_size)
×
1382

1383

1384
@dataclass
1 ✔
1385
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1 ✔
1386
    """The first derivative band neighborhood aggregate."""
1387

1388
    equally_spaced_band_distance: float
1 ✔
1389

1390
    @classmethod
1 ✔
1391
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1 ✔
NEW
1392
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
NEW
1393
            raise ValueError("Invalid neighborhood aggregate type")
×
1394

NEW
1395
        return BandNeighborhoodAggregateFirstDerivative(band_neighborhood_aggregate_dict["bandDistance"]["distance"])
×
1396

1397
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1398
        return {
×
1399
            "type": "firstDerivative",
1400
            "bandDistance": {"type": "equallySpaced", "distance": self.equally_spaced_band_distance},
1401
        }
1402

1403

1404
@dataclass
1 ✔
1405
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1 ✔
1406
    """The average band neighborhood aggregate."""
1407

1408
    window_size: int
1 ✔
1409

1410
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1411
        return {"type": "average", "windowSize": self.window_size}
×
1412

1413

1414
class Onnx(RasterOperator):
1 ✔
1415
    """Onnx ML operator."""
1416

1417
    source: RasterOperator
1 ✔
1418
    model: str
1 ✔
1419

1420
    # pylint: disable=too-many-arguments
1421
    def __init__(self, source: RasterOperator, model: str):
1 ✔
1422
        """Creates a new Onnx operator."""
1423
        self.source = source
1 ✔
1424
        self.model = model
1 ✔
1425

1426
    def name(self) -> str:
1 ✔
1427
        return "Onnx"
1 ✔
1428

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

1432
    @classmethod
1 ✔
1433
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Onnx:
1 ✔
1434
        if operator_dict["type"] != "Onnx":
1 ✔
NEW
1435
            raise ValueError("Invalid operator type")
×
1436

1437
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1 ✔
1438
        model = operator_dict["params"]["model"]
1 ✔
1439

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

© 2026 Coveralls, Inc