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

geo-engine / geoengine / 43082439754

11 Nov 2026 06:55PM UTC coverage: 88.091% (+0.1%) from 87.996%
43082439754

push

github

web-flow
feat(backend): add PolygonRasterization operator (#1434)

* feat(backend): describe raster tiles as ASCII art in tests

* feat(backend): accept dates and calendar steps in ASCII raster tiles

* refactor(backend): describe raster tiles as ASCII art in operator tests

* refactor(backend): rename origin method to tile_origin for clarity in ASCII raster tiles

* feat(backend): add PolygonRasterization operator

* feat(ui): add Polygon Rasterization component and integrate into operator list

* refactor(backend): extract shared pixel aggregation for temporal aggregation and polygon rasterization

* refactor(backend): replace PixelGrid by GeoTransform and GridShape2D

* docs(backend): clarify feature time slices of PolygonRasterization

* docs(backend): document how resolution and origin map to the pixel grid

* feat(python): add PolygonRasterization operator

* docs(www): regenerate Python docs for PolygonRasterization operator

1297 of 1357 new or added lines in 11 files covered. (95.58%)

5 existing lines in 2 files now uncovered.

139135 of 157945 relevant lines covered (88.09%)

532238.63 hits per line

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

82.67
/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

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 ✔
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 ✔
89
            return Reprojection.from_operator_dict(operator_dict).as_raster()
×
90
        if operator_dict["type"] == "Downsampling":
1 ✔
91
            return Downsampling.from_operator_dict(operator_dict)
×
92
        if operator_dict["type"] == "Interpolation":
1 ✔
93
            return Interpolation.from_operator_dict(operator_dict)
×
94
        if operator_dict["type"] == "Expression":
1 ✔
95
            return Expression.from_operator_dict(operator_dict)
×
96
        if operator_dict["type"] == "BandwiseExpression":
1 ✔
97
            return BandwiseExpression.from_operator_dict(operator_dict)
×
98
        if operator_dict["type"] == "TimeShift":
1 ✔
99
            return TimeShift.from_operator_dict(operator_dict).as_raster()
×
100
        if operator_dict["type"] == "TemporalRasterAggregation":
1 ✔
101
            return TemporalRasterAggregation.from_operator_dict(operator_dict)
×
102
        if operator_dict["type"] == "RasterStacker":
1 ✔
103
            return RasterStacker.from_operator_dict(operator_dict)
×
104
        if operator_dict["type"] == "BandFilter":
1 ✔
105
            return BandFilter.from_operator_dict(operator_dict)
×
106
        if operator_dict["type"] == "BandNeighborhoodAggregate":
1 ✔
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
        if operator_dict["type"] == "PolygonRasterization":
1 ✔
111
            return PolygonRasterization.from_operator_dict(operator_dict)
1 ✔
112

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

115

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

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

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

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

143

144
class GdalSource(RasterOperator):
1 ✔
145
    """A GDAL source operator."""
146

147
    dataset: str
1 ✔
148

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

155
    def name(self) -> str:
1 ✔
156
        return "GdalSource"
1 ✔
157

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

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

167
        return GdalSource(cast(str, operator_dict["params"]["data"]))
1 ✔
168

169

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

173
    dataset: str
1 ✔
174

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

181
    def name(self) -> str:
1 ✔
182
        return "MultiBandGdalSource"
1 ✔
183

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

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

193
        return MultiBandGdalSource(cast(str, operator_dict["params"]["data"]))
1 ✔
194

195

196
class OgrSource(VectorOperator):
1 ✔
197
    """An OGR source operator."""
198

199
    dataset: str
1 ✔
200
    attribute_projection: str | None = None
1 ✔
201
    attribute_filters: str | None = None
1 ✔
202

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

216
    def name(self) -> str:
1 ✔
217
        return "OgrSource"
1 ✔
218

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

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

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

242

243
class Interpolation(RasterOperator):
1 ✔
244
    """An interpolation operator."""
245

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

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

271
    def name(self) -> str:
1 ✔
272
        return "Interpolation"
1 ✔
273

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

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

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

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

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

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

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

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

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

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

344
        return op
1 ✔
345

346

347
class Downsampling(RasterOperator):
1 ✔
348
    """A Downsampling operator."""
349

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

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

375
    def name(self) -> str:
1 ✔
376
        return "Downsampling"
1 ✔
377

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

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

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

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

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

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

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

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

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

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

449
        return op
1 ✔
450

451

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

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

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

470
    @classmethod
1 ✔
471
    def default(cls) -> ColumnNames:
1 ✔
472
        return ColumnNamesDefault()
×
473

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

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

482

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

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

489

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

493
    suffixes: list[str]
1 ✔
494

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

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

502

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

506
    new_names: list[str]
1 ✔
507

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

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

515

516
class RasterVectorJoin(VectorOperator):
1 ✔
517
    """A RasterVectorJoin operator."""
518

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

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

547
    def name(self) -> str:
1 ✔
548
        return "RasterVectorJoin"
1 ✔
549

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

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

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

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

589

590
class PointInPolygonFilter(VectorOperator):
1 ✔
591
    """A PointInPolygonFilter operator."""
592

593
    point_source: VectorOperator
1 ✔
594
    polygon_source: VectorOperator
1 ✔
595

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

605
    def name(self) -> str:
1 ✔
606
        return "PointInPolygonFilter"
1 ✔
607

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

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

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

624
        return PointInPolygonFilter(
1 ✔
625
            point_source=point_source,
626
            polygon_source=polygon_source,
627
        )
628

629

630
class RasterScaling(RasterOperator):
1 ✔
631
    """A RasterScaling operator.
632

633
    This operator scales the values of a raster by a given slope and offset.
634

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

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

641
    """
642

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

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

667
    def name(self) -> str:
1 ✔
668
        return "RasterScaling"
1 ✔
669

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

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

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

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

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

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

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

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

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

719

720
class RasterTypeConversion(RasterOperator):
1 ✔
721
    """A RasterTypeConversion operator."""
722

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

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

735
    def name(self) -> str:
1 ✔
736
        return "RasterTypeConversion"
1 ✔
737

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

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

750
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1 ✔
751

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

754

755
class Reprojection(Operator):
1 ✔
756
    """A Reprojection operator."""
757

758
    source: Operator
1 ✔
759
    target_spatial_reference: str
1 ✔
760

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

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

769
    def name(self) -> str:
1 ✔
770
        return "Reprojection"
1 ✔
771

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

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

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

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

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

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

808

809
class Expression(RasterOperator):
1 ✔
810
    """An Expression operator."""
811

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

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

834
    def name(self) -> str:
1 ✔
835
        return "Expression"
1 ✔
836

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

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

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

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

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

870

871
class BandwiseExpression(RasterOperator):
1 ✔
872
    """A bandwise Expression operator."""
873

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

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

893
    def name(self) -> str:
1 ✔
894
        return "BandwiseExpression"
×
895

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

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

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

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

917

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

921
    MULTI_POINT = "MultiPoint"
1 ✔
922
    MULTI_LINE_STRING = "MultiLineString"
1 ✔
923
    MULTI_POLYGON = "MultiPolygon"
1 ✔
924

925

926
class VectorExpression(VectorOperator):
1 ✔
927
    """The `VectorExpression` operator."""
928

929
    source: VectorOperator
1 ✔
930

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

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

951
        self.expression = expression
1 ✔
952
        self.input_columns = input_columns
1 ✔
953
        self.output_column = output_column
1 ✔
954

955
        self.geometry_column_name = geometry_column_name
1 ✔
956
        self.output_measurement = output_measurement
1 ✔
957

958
    def name(self) -> str:
1 ✔
959
        return "VectorExpression"
1 ✔
960

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

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

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

985
        if self.geometry_column_name:
1 ✔
986
            params["geometryColumnName"] = self.geometry_column_name
1 ✔
987

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

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

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

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

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

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

1017

1018
class TemporalRasterAggregation(RasterOperator):
1 ✔
1019
    """A TemporalRasterAggregation operator."""
1020

1021
    # pylint: disable=too-many-instance-attributes
1022

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

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

1070
    def name(self) -> str:
1 ✔
1071
        return "TemporalRasterAggregation"
1 ✔
1072

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

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

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

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

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

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

1119

1120
class TimeShift(Operator):
1 ✔
1121
    """A RasterTypeConversion operator."""
1122

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

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

1143
    def name(self) -> str:
1 ✔
1144
        return "TimeShift"
1 ✔
1145

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

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

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

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

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

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

1186

1187
class RenameBands:
1 ✔
1188
    """Base class for renaming bands of a raster."""
1189

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

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

1205
    @classmethod
1 ✔
1206
    def default(cls) -> RenameBands:
1 ✔
1207
        return RenameBandsDefault()
×
1208

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

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

1217

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

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

1224

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

1228
    suffixes: list[str]
1 ✔
1229

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

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

1237

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

1241
    new_names: list[str]
1 ✔
1242

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

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

1250

1251
class RasterStacker(RasterOperator):
1 ✔
1252
    """The RasterStacker operator."""
1253

1254
    sources: list[RasterOperator]
1 ✔
1255
    rename: RenameBands
1 ✔
1256

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

1263
        self.sources = sources
1 ✔
1264
        self.rename = rename
1 ✔
1265

1266
    def name(self) -> str:
1 ✔
1267
        return "RasterStacker"
1 ✔
1268

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

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

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

1284
        return RasterStacker(sources=sources, rename=rename)
1 ✔
1285

1286

1287
class BandFilter(RasterOperator):
1 ✔
1288
    """The BandFilter operator.
1289

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

1295
    source: RasterOperator
1 ✔
1296
    bands: list[str] | list[int]
1 ✔
1297

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

1303
        self.source = source
1 ✔
1304
        self.bands = bands
1 ✔
1305

1306
    def name(self) -> str:
1 ✔
1307
        return "BandFilter"
1 ✔
1308

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

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

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

1325
        return BandFilter(source=source, bands=bands)
1 ✔
1326

1327

1328
class BandNeighborhoodAggregate(RasterOperator):
1 ✔
1329
    """The BandNeighborhoodAggregate operator."""
1330

1331
    source: RasterOperator
1 ✔
1332
    aggregate: BandNeighborhoodAggregateParams
1 ✔
1333

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

1340
    def name(self) -> str:
1 ✔
1341
        return "BandNeighborhoodAggregate"
×
1342

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

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

1355
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1356
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1357

1358
        return BandNeighborhoodAggregate(source=source, aggregate=aggregate)
×
1359

1360

1361
class BandNeighborhoodAggregateParams:
1 ✔
1362
    """Abstract base class for band neighborhood aggregate params."""
1363

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

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

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

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

1385

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

1390
    equally_spaced_band_distance: float
1 ✔
1391

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

1397
        return BandNeighborhoodAggregateFirstDerivative(band_neighborhood_aggregate_dict["bandDistance"]["distance"])
×
1398

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

1405

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

1410
    window_size: int
1 ✔
1411

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

1415

1416
class Onnx(RasterOperator):
1 ✔
1417
    """Onnx ML operator."""
1418

1419
    source: RasterOperator
1 ✔
1420
    model: str
1 ✔
1421

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

1428
    def name(self) -> str:
1 ✔
1429
        return "Onnx"
1 ✔
1430

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

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

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

1442
        return Onnx(source=source, model=model)
1 ✔
1443

1444

1445
PolygonRasterizationAggregation = Literal["first", "last", "min", "max", "sum", "mean", "medianEstimate"]
1 ✔
1446
TimeGranularity = Literal["millis", "seconds", "minutes", "hours", "days", "months", "years"]
1 ✔
1447

1448

1449
class PolygonRasterizationValue:
1 ✔
1450
    """The value that a polygon burns into the pixels it covers."""
1451

1452
    @abstractmethod
1 ✔
1453
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1454
        pass
×
1455

1456
    @classmethod
1 ✔
1457
    def from_dict(cls, value_dict: dict[str, Any]) -> PolygonRasterizationValue:
1 ✔
1458
        """Returns a value object from a dictionary."""
1459
        if value_dict["type"] == "constant":
1 ✔
1460
            return PolygonRasterizationValueConstant(float(value_dict["value"]))
1 ✔
1461
        if value_dict["type"] == "count":
1 ✔
1462
            return PolygonRasterizationValueCount()
1 ✔
1463
        if value_dict["type"] == "attribute":
1 ✔
1464
            return PolygonRasterizationValueAttribute(
1 ✔
1465
                column=value_dict["column"],
1466
                aggregation=value_dict["aggregation"],
1467
                ignore_no_data=value_dict.get("ignoreNoData", True),
1468
            )
1469
        raise ValueError(f"Invalid polygon rasterization value type {value_dict['type']}")
1 ✔
1470

1471
    @classmethod
1 ✔
1472
    def constant(cls, value: float) -> PolygonRasterizationValue:
1 ✔
1473
        """Each covered pixel gets the `value`."""
1474
        return PolygonRasterizationValueConstant(value)
1 ✔
1475

1476
    @classmethod
1 ✔
1477
    def count(cls) -> PolygonRasterizationValue:
1 ✔
1478
        """Each pixel gets the number of polygons that cover it."""
1479
        return PolygonRasterizationValueCount()
1 ✔
1480

1481
    @classmethod
1 ✔
1482
    def attribute(
1 ✔
1483
        cls, column: str, aggregation: PolygonRasterizationAggregation, ignore_no_data: bool = True
1484
    ) -> PolygonRasterizationValue:
1485
        """Each pixel gets the value of a numeric column, aggregated over all polygons that cover it.
1486

1487
        If `ignore_no_data` is `False`, features with a null value make all pixels they cover no data.
1488
        """
1489
        return PolygonRasterizationValueAttribute(column, aggregation, ignore_no_data)
1 ✔
1490

1491

1492
@dataclass
1 ✔
1493
class PolygonRasterizationValueConstant(PolygonRasterizationValue):
1 ✔
1494
    """Each covered pixel gets the constant value."""
1495

1496
    value: float
1 ✔
1497

1498
    def to_dict(self) -> dict[str, Any]:
1 ✔
1499
        return {"type": "constant", "value": self.value}
1 ✔
1500

1501

1502
@dataclass
1 ✔
1503
class PolygonRasterizationValueCount(PolygonRasterizationValue):
1 ✔
1504
    """Each pixel gets the number of polygons that cover it."""
1505

1506
    def to_dict(self) -> dict[str, Any]:
1 ✔
1507
        return {"type": "count"}
1 ✔
1508

1509

1510
@dataclass
1 ✔
1511
class PolygonRasterizationValueAttribute(PolygonRasterizationValue):
1 ✔
1512
    """Each pixel gets the value of a numeric column, aggregated over all polygons that cover it."""
1513

1514
    column: str
1 ✔
1515
    aggregation: PolygonRasterizationAggregation
1 ✔
1516
    ignore_no_data: bool = True
1 ✔
1517

1518
    def to_dict(self) -> dict[str, Any]:
1 ✔
1519
        return {
1 ✔
1520
            "type": "attribute",
1521
            "column": self.column,
1522
            "aggregation": self.aggregation,
1523
            "ignoreNoData": self.ignore_no_data,
1524
        }
1525

1526

1527
class PolygonRasterizationTime:
1 ✔
1528
    """How the time slices of the output raster are derived from the features."""
1529

1530
    @abstractmethod
1 ✔
1531
    def to_dict(self) -> dict[str, Any]:
1 ✔
NEW
1532
        pass
×
1533

1534
    @classmethod
1 ✔
1535
    def from_dict(cls, time_dict: dict[str, Any]) -> PolygonRasterizationTime:
1 ✔
1536
        """Returns a time object from a dictionary."""
1537
        if time_dict["type"] == "featureTimes":
1 ✔
1538
            return PolygonRasterizationTimeFeatureTimes()
1 ✔
1539
        if time_dict["type"] == "regular":
1 ✔
1540
            reference = time_dict.get("reference")
1 ✔
1541
            return PolygonRasterizationTimeRegular(
1 ✔
1542
                granularity=time_dict["step"]["granularity"],
1543
                step=time_dict["step"]["step"],
1544
                reference=np.datetime64(reference, "ms") if reference is not None else None,
1545
            )
1546
        if time_dict["type"] == "ignore":
1 ✔
1547
            return PolygonRasterizationTimeIgnore()
1 ✔
NEW
1548
        raise ValueError(f"Invalid polygon rasterization time type {time_dict['type']}")
×
1549

1550
    @classmethod
1 ✔
1551
    def feature_times(cls) -> PolygonRasterizationTime:
1 ✔
1552
        """Every start and end time of a feature is a slice boundary."""
1553
        return PolygonRasterizationTimeFeatureTimes()
1 ✔
1554

1555
    @classmethod
1 ✔
1556
    def regular(
1 ✔
1557
        cls,
1558
        granularity: TimeGranularity,
1559
        step: int = 1,
1560
        reference: datetime.datetime | np.datetime64 | None = None,
1561
    ) -> PolygonRasterizationTime:
1562
        """Regular time slices of length `step`, aligned to `reference` (default: Unix epoch)."""
1563
        if isinstance(reference, datetime.datetime):
1 ✔
1564
            # We assume that a datetime without a timezone means UTC
1565
            if reference.tzinfo is not None:
1 ✔
1566
                reference = reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
1 ✔
1567
            reference = np.datetime64(reference)
1 ✔
1568
        return PolygonRasterizationTimeRegular(granularity, step, reference)
1 ✔
1569

1570
    @classmethod
1 ✔
1571
    def ignore(cls) -> PolygonRasterizationTime:
1 ✔
1572
        """All features are rasterized into one time slice that is valid forever."""
1573
        return PolygonRasterizationTimeIgnore()
1 ✔
1574

1575

1576
@dataclass
1 ✔
1577
class PolygonRasterizationTimeFeatureTimes(PolygonRasterizationTime):
1 ✔
1578
    """Every start and end time of a feature is a slice boundary."""
1579

1580
    def to_dict(self) -> dict[str, Any]:
1 ✔
1581
        return {"type": "featureTimes"}
1 ✔
1582

1583

1584
@dataclass
1 ✔
1585
class PolygonRasterizationTimeRegular(PolygonRasterizationTime):
1 ✔
1586
    """Regular time slices that contain all features whose time intersects them."""
1587

1588
    granularity: TimeGranularity
1 ✔
1589
    step: int = 1
1 ✔
1590
    reference: np.datetime64 | None = None
1 ✔
1591

1592
    def to_dict(self) -> dict[str, Any]:
1 ✔
1593
        time_dict: dict[str, Any] = {
1 ✔
1594
            "type": "regular",
1595
            "step": {"granularity": self.granularity, "step": self.step},
1596
        }
1597
        if self.reference is not None:
1 ✔
1598
            time_dict["reference"] = int(self.reference.astype("datetime64[ms]").astype(int))
1 ✔
1599
        return time_dict
1 ✔
1600

1601

1602
@dataclass
1 ✔
1603
class PolygonRasterizationTimeIgnore(PolygonRasterizationTime):
1 ✔
1604
    """All features are rasterized into one time slice that is valid forever."""
1605

1606
    def to_dict(self) -> dict[str, Any]:
1 ✔
1607
        return {"type": "ignore"}
1 ✔
1608

1609

1610
class PolygonRasterization(RasterOperator):
1 ✔
1611
    """The `PolygonRasterization` operator, which rasterizes (multi) polygons.
1612

1613
    A pixel is covered by a polygon if its center lies inside the polygon.
1614
    Pixels that are not covered by any polygon are no data.
1615
    """
1616

1617
    # pylint: disable=too-many-instance-attributes
1618

1619
    source: VectorOperator
1 ✔
1620
    spatial_resolution: tuple[float, float]
1 ✔
1621
    origin_coordinate: tuple[float, float] | None
1 ✔
1622
    value: PolygonRasterizationValue | None
1 ✔
1623
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None
1 ✔
1624
    output_measurement: Measurement | None
1 ✔
1625
    time: PolygonRasterizationTime | None
1 ✔
1626

1627
    # pylint: disable=too-many-arguments
1628
    def __init__(
1 ✔
1629
        self,
1630
        source: VectorOperator,
1631
        *,
1632
        spatial_resolution: tuple[float, float],
1633
        origin_coordinate: tuple[float, float] | None = None,
1634
        value: PolygonRasterizationValue | None = None,
1635
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None,
1636
        output_measurement: Measurement | None = None,
1637
        time: PolygonRasterizationTime | None = None,
1638
    ):
1639
        """Creates a new PolygonRasterization operator.
1640

1641
        `spatial_resolution` are the positive pixel sizes in x and y direction.
1642
        The rows of the grid run from north to south, and `origin_coordinate` is a pixel corner
1643
        to which the grid is aligned (default: `(0, 0)`).
1644
        Omitted parameters use the defaults of the server, i.e., a constant value of `1` and
1645
        time slices between the start and end times of the features.
1646
        """
1647
        self.source = source
1 ✔
1648
        self.spatial_resolution = spatial_resolution
1 ✔
1649
        self.origin_coordinate = origin_coordinate
1 ✔
1650
        self.value = value
1 ✔
1651
        self.output_type = output_type
1 ✔
1652
        self.output_measurement = output_measurement
1 ✔
1653
        self.time = time
1 ✔
1654

1655
    def name(self) -> str:
1 ✔
1656
        return "PolygonRasterization"
1 ✔
1657

1658
    def to_dict(self) -> dict[str, Any]:
1 ✔
1659
        params: dict[str, Any] = {
1 ✔
1660
            "spatialResolution": {"x": self.spatial_resolution[0], "y": self.spatial_resolution[1]},
1661
        }
1662
        if self.origin_coordinate is not None:
1 ✔
1663
            params["originCoordinate"] = {"x": self.origin_coordinate[0], "y": self.origin_coordinate[1]}
1 ✔
1664
        if self.value is not None:
1 ✔
1665
            params["value"] = self.value.to_dict()
1 ✔
1666
        if self.output_type is not None:
1 ✔
1667
            params["outputType"] = self.output_type
1 ✔
1668
        if self.output_measurement is not None:
1 ✔
1669
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
1 ✔
1670
        if self.time is not None:
1 ✔
1671
            params["time"] = self.time.to_dict()
1 ✔
1672

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

1675
    @classmethod
1 ✔
1676
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> PolygonRasterization:
1 ✔
1677
        if operator_dict["type"] != "PolygonRasterization":
1 ✔
NEW
1678
            raise ValueError("Invalid operator type")
×
1679

1680
        params = operator_dict["params"]
1 ✔
1681

1682
        origin_coordinate = None
1 ✔
1683
        if params.get("originCoordinate") is not None:
1 ✔
1684
            origin_coordinate = (float(params["originCoordinate"]["x"]), float(params["originCoordinate"]["y"]))
1 ✔
1685

1686
        output_measurement = None
1 ✔
1687
        if params.get("outputMeasurement") is not None:
1 ✔
1688
            output_measurement = Measurement.from_response(
1 ✔
1689
                geoengine_api_client.Measurement.from_dict(params["outputMeasurement"])
1690
            )
1691

1692
        return PolygonRasterization(
1 ✔
1693
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
1694
            spatial_resolution=(float(params["spatialResolution"]["x"]), float(params["spatialResolution"]["y"])),
1695
            origin_coordinate=origin_coordinate,
1696
            value=PolygonRasterizationValue.from_dict(params["value"]) if params.get("value") is not None else None,
1697
            output_type=params.get("outputType"),
1698
            output_measurement=output_measurement,
1699
            time=PolygonRasterizationTime.from_dict(params["time"]) if params.get("time") is not None else None,
1700
        )
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