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

geo-engine / geoengine / 17036501662

18 Aug 2025 09:22AM UTC coverage: 88.073%. First build
17036501662

Pull #1015

github

web-flow
Merge a8157bfec into db8685e5e
Pull Request #1015: workflow optimization (wip)

1664 of 2176 new or added lines in 51 files covered. (76.47%)

114926 of 130490 relevant lines covered (88.07%)

179859.01 hits per line

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

89.26
/operators/src/processing/raster_scaling.rs
1
use crate::engine::{
2
    CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources, Operator,
3
    OperatorName, RasterBandDescriptor, RasterOperator, RasterQueryProcessor,
4
    RasterResultDescriptor, SingleRasterSource, TypedRasterQueryProcessor, WorkflowOperatorPath,
5
};
6
use crate::optimization::OptimizationError;
7
use crate::util::Result;
8
use async_trait::async_trait;
9
use futures::{StreamExt, TryStreamExt};
10

11
use geoengine_datatypes::primitives::SpatialResolution;
12
use geoengine_datatypes::raster::{
13
    CheckedMulThenAddTransformation, CheckedSubThenDivTransformation, ElementScaling,
14
    ScalingTransformation,
15
};
16
use geoengine_datatypes::{
17
    primitives::Measurement,
18
    raster::{Pixel, RasterPropertiesKey, RasterTile2D},
19
};
20
use num::FromPrimitive;
21
use num_traits::AsPrimitive;
22
use rayon::ThreadPool;
23
use serde::{Deserialize, Serialize};
24
use std::marker::PhantomData;
25
use std::sync::Arc;
26

27
#[derive(Debug, Serialize, Deserialize, Clone)]
28
#[serde(rename_all = "camelCase")]
29
pub struct RasterScalingParams {
30
    slope: SlopeOffsetSelection,
31
    offset: SlopeOffsetSelection,
32
    output_measurement: Option<Measurement>,
33
    scaling_mode: ScalingMode,
34
}
35

36
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
37
#[serde(rename_all = "camelCase")]
38
pub enum ScalingMode {
39
    MulSlopeAddOffset,
40
    SubOffsetDivSlope,
41
}
42

43
#[derive(Debug, Serialize, Deserialize, Clone)]
44
#[serde(rename_all = "camelCase", tag = "type")]
45
enum SlopeOffsetSelection {
46
    Auto,
47
    MetadataKey(RasterPropertiesKey),
48
    Constant { value: f64 },
49
}
50

51
impl Default for SlopeOffsetSelection {
52
    fn default() -> Self {
×
53
        Self::Auto
×
54
    }
×
55
}
56

57
/// The raster scaling operator scales/unscales the values of a raster by a given scale factor and offset.
58
/// This is done by applying the following formulas to every pixel.
59
/// For unscaling the formula is:
60
///
61
/// `p_new = p_old * slope + offset`
62
///
63
/// For scaling the formula is:
64
///
65
/// `p_new = (p_old - offset) / slope`
66
///
67
/// `p_old` and `p_new` refer to the old and new pixel values,
68
/// The slope and offset values are either properties attached to the input raster or a fixed value.
69
///
70
/// An example for Meteosat Second Generation properties is:
71
///
72
/// - offset: `msg.calibration_offset`
73
/// - slope: `msg.calibration_slope`
74
pub type RasterScaling = Operator<RasterScalingParams, SingleRasterSource>;
75

76
impl OperatorName for RasterScaling {
77
    const TYPE_NAME: &'static str = "RasterScaling";
78
}
79

80
pub struct InitializedRasterScalingOperator {
81
    name: CanonicOperatorName,
82
    path: WorkflowOperatorPath,
83
    slope: SlopeOffsetSelection,
84
    offset: SlopeOffsetSelection,
85
    result_descriptor: RasterResultDescriptor,
86
    source: Box<dyn InitializedRasterOperator>,
87
    scaling_mode: ScalingMode,
88
}
89

90
#[typetag::serde]
×
91
#[async_trait]
92
impl RasterOperator for RasterScaling {
93
    async fn _initialize(
94
        self: Box<Self>,
95
        path: WorkflowOperatorPath,
96
        context: &dyn ExecutionContext,
97
    ) -> Result<Box<dyn InitializedRasterOperator>> {
4✔
98
        let name = CanonicOperatorName::from(&self);
2✔
99

100
        let input = self
2✔
101
            .sources
2✔
102
            .initialize_sources(path.clone(), context)
2✔
103
            .await?;
2✔
104
        let in_desc = input.raster.result_descriptor();
2✔
105

106
        let out_desc = RasterResultDescriptor {
2✔
107
            spatial_reference: in_desc.spatial_reference,
2✔
108
            data_type: in_desc.data_type,
2✔
109
            time: in_desc.time,
2✔
110
            spatial_grid: in_desc.spatial_grid,
2✔
111
            bands: in_desc
2✔
112
                .bands
2✔
113
                .iter()
2✔
114
                .map(|b| {
2✔
115
                    RasterBandDescriptor::new(
2✔
116
                        b.name.clone(),
2✔
117
                        self.params
2✔
118
                            .output_measurement
2✔
119
                            .clone()
2✔
120
                            .unwrap_or_else(|| b.measurement.clone()),
2✔
121
                    )
122
                })
2✔
123
                .collect::<Vec<_>>()
2✔
124
                .try_into()?,
2✔
125
        };
126

127
        let initialized_operator = InitializedRasterScalingOperator {
2✔
128
            name,
2✔
129
            path,
2✔
130
            slope: self.params.slope,
2✔
131
            offset: self.params.offset,
2✔
132
            result_descriptor: out_desc,
2✔
133
            source: input.raster,
2✔
134
            scaling_mode: self.params.scaling_mode,
2✔
135
        };
2✔
136

137
        Ok(initialized_operator.boxed())
2✔
138
    }
4✔
139

140
    span_fn!(RasterScaling);
141
}
142

143
impl InitializedRasterOperator for InitializedRasterScalingOperator {
144
    fn result_descriptor(&self) -> &RasterResultDescriptor {
2✔
145
        &self.result_descriptor
2✔
146
    }
2✔
147

148
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
2✔
149
        let slope = self.slope.clone();
2✔
150
        let offset = self.offset.clone();
2✔
151
        let source = self.source.query_processor()?;
2✔
152
        let scaling_mode = self.scaling_mode;
2✔
153

154
        let res = match scaling_mode {
2✔
155
            ScalingMode::SubOffsetDivSlope => {
156
                call_on_generic_raster_processor!(source, source_proc => { TypedRasterQueryProcessor::from(create_boxed_processor::<_,_, CheckedSubThenDivTransformation>(self.result_descriptor.clone(), slope, offset,  source_proc)) })
1✔
157
            }
158
            ScalingMode::MulSlopeAddOffset => {
159
                call_on_generic_raster_processor!(source, source_proc => { TypedRasterQueryProcessor::from(create_boxed_processor::<_,_, CheckedMulThenAddTransformation>(self.result_descriptor.clone(), slope, offset,  source_proc)) })
1✔
160
            }
161
        };
162

163
        Ok(res)
2✔
164
    }
2✔
165

166
    fn canonic_name(&self) -> CanonicOperatorName {
×
167
        self.name.clone()
×
168
    }
×
169

170
    fn name(&self) -> &'static str {
×
171
        RasterScaling::TYPE_NAME
×
172
    }
×
173

174
    fn path(&self) -> WorkflowOperatorPath {
×
175
        self.path.clone()
×
176
    }
×
177

NEW
178
    fn optimize(
×
NEW
179
        &self,
×
NEW
180
        target_resolution: SpatialResolution,
×
NEW
181
    ) -> Result<Box<dyn RasterOperator>, OptimizationError> {
×
182
        Ok(RasterScaling {
NEW
183
            params: RasterScalingParams {
×
NEW
184
                slope: self.slope.clone(),
×
NEW
185
                offset: self.offset.clone(),
×
NEW
186
                output_measurement: Some(self.result_descriptor.bands[0].measurement.clone()),
×
NEW
187
                scaling_mode: self.scaling_mode,
×
NEW
188
            },
×
189
            sources: SingleRasterSource {
NEW
190
                raster: self.source.optimize(target_resolution)?,
×
191
            },
192
        }
NEW
193
        .boxed())
×
NEW
194
    }
×
195
}
196

197
struct RasterTransformationProcessor<Q, P, S>
198
where
199
    Q: RasterQueryProcessor<RasterType = P>,
200
{
201
    source: Q,
202
    result_descriptor: RasterResultDescriptor,
203
    slope: SlopeOffsetSelection,
204
    offset: SlopeOffsetSelection,
205
    _transformation: PhantomData<S>,
206
}
207

208
fn create_boxed_processor<Q, P, S>(
2✔
209
    result_descriptor: RasterResultDescriptor,
2✔
210
    slope: SlopeOffsetSelection,
2✔
211
    offset: SlopeOffsetSelection,
2✔
212
    source: Q,
2✔
213
) -> Box<dyn RasterQueryProcessor<RasterType = P>>
2✔
214
where
2✔
215
    Q: RasterQueryProcessor<RasterType = P> + 'static,
2✔
216
    P: Pixel + FromPrimitive + 'static + Default,
2✔
217
    f64: AsPrimitive<P>,
2✔
218
    S: Send + Sync + 'static + ScalingTransformation<P>,
2✔
219
{
220
    RasterTransformationProcessor::<Q, P, S>::create(result_descriptor, slope, offset, source)
2✔
221
        .boxed()
2✔
222
}
2✔
223

224
impl<Q, P, S> RasterTransformationProcessor<Q, P, S>
225
where
226
    Q: RasterQueryProcessor<RasterType = P> + 'static,
227
    P: Pixel + FromPrimitive + 'static + Default,
228
    f64: AsPrimitive<P>,
229
    S: Send + Sync + 'static + ScalingTransformation<P>,
230
{
231
    pub fn create(
2✔
232
        result_descriptor: RasterResultDescriptor,
2✔
233
        slope: SlopeOffsetSelection,
2✔
234
        offset: SlopeOffsetSelection,
2✔
235
        source: Q,
2✔
236
    ) -> RasterTransformationProcessor<Q, P, S> {
2✔
237
        RasterTransformationProcessor {
2✔
238
            source,
2✔
239
            result_descriptor,
2✔
240
            slope,
2✔
241
            offset,
2✔
242
            _transformation: PhantomData,
2✔
243
        }
2✔
244
    }
2✔
245

246
    async fn scale_tile_async(
2✔
247
        &self,
2✔
248
        tile: RasterTile2D<P>,
2✔
249
        _pool: Arc<ThreadPool>,
2✔
250
    ) -> Result<RasterTile2D<P>> {
2✔
251
        // either use the provided metadata/constant or the default values from the properties
252
        let offset = match &self.offset {
2✔
253
            SlopeOffsetSelection::MetadataKey(key) => tile.properties.number_property::<P>(key)?,
×
254
            SlopeOffsetSelection::Constant { value } => value.as_(),
×
255
            SlopeOffsetSelection::Auto => tile.properties.offset().as_(),
2✔
256
        };
257

258
        let slope = match &self.slope {
2✔
259
            SlopeOffsetSelection::MetadataKey(key) => tile.properties.number_property::<P>(key)?,
×
260
            SlopeOffsetSelection::Constant { value } => value.as_(),
×
261
            SlopeOffsetSelection::Auto => tile.properties.scale().as_(),
2✔
262
        };
263

264
        let res_tile =
2✔
265
            crate::util::spawn_blocking(move || tile.transform_elements::<S>(slope, offset))
2✔
266
                .await?;
2✔
267

268
        Ok(res_tile)
2✔
269
    }
2✔
270
}
271

272
#[async_trait]
273
impl<Q, P, S> RasterQueryProcessor for RasterTransformationProcessor<Q, P, S>
274
where
275
    P: Pixel + FromPrimitive + 'static + Default,
276
    f64: AsPrimitive<P>,
277
    Q: RasterQueryProcessor<RasterType = P> + 'static,
278
    S: Send + Sync + 'static + ScalingTransformation<P>,
279
{
280
    type RasterType = P;
281

282
    async fn raster_query<'a>(
283
        &'a self,
284
        query: geoengine_datatypes::primitives::RasterQueryRectangle,
285
        ctx: &'a dyn crate::engine::QueryContext,
286
    ) -> Result<
287
        futures::stream::BoxStream<
288
            'a,
289
            Result<geoengine_datatypes::raster::RasterTile2D<Self::RasterType>>,
290
        >,
291
    > {
4✔
292
        let src = self.source.raster_query(query, ctx).await?;
2✔
293
        let rs = src.and_then(move |tile| self.scale_tile_async(tile, ctx.thread_pool().clone()));
2✔
294
        Ok(rs.boxed())
2✔
295
    }
4✔
296

297
    fn raster_result_descriptor(&self) -> &RasterResultDescriptor {
2✔
298
        &self.result_descriptor
2✔
299
    }
2✔
300
}
301

302
#[cfg(test)]
303
mod tests {
304

305
    use crate::{
306
        engine::{
307
            ChunkByteSize, MockExecutionContext, RasterBandDescriptors, SpatialGridDescriptor,
308
        },
309
        mock::{MockRasterSource, MockRasterSourceParams},
310
    };
311
    use geoengine_datatypes::{
312
        primitives::{BandSelection, CacheHint, Coordinate2D, TimeInterval},
313
        raster::{
314
            BoundedGrid, GeoTransform, Grid2D, GridBoundingBox2D, GridOrEmpty2D, GridShape,
315
            GridShape2D, MaskedGrid2D, RasterDataType, RasterProperties, TileInformation,
316
            TilingSpecification,
317
        },
318
        spatial_reference::SpatialReference,
319
        util::test::TestDefault,
320
    };
321

322
    use super::*;
323

324
    #[tokio::test]
325
    async fn test_unscale() {
1✔
326
        let tile_size_in_pixels = GridShape2D::new_2d(2, 2);
1✔
327
        let result_descriptor = RasterResultDescriptor {
1✔
328
            data_type: RasterDataType::U8,
1✔
329
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
330
            time: None,
1✔
331
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
332
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
333
                tile_size_in_pixels.bounding_box(),
1✔
334
            ),
1✔
335
            bands: RasterBandDescriptors::new_single_band(),
1✔
336
        };
1✔
337

338
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
339
        let raster =
1✔
340
            MaskedGrid2D::from(Grid2D::new(tile_size_in_pixels, vec![7_u8, 7, 7, 6]).unwrap());
1✔
341

342
        let ctx = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
343
        let query_ctx = ctx.mock_query_context(ChunkByteSize::test_default());
1✔
344

345
        let mut raster_props = RasterProperties::default();
1✔
346
        raster_props.set_scale(2.0);
1✔
347
        raster_props.set_offset(1.0);
1✔
348

349
        let raster_tile = RasterTile2D::new_with_tile_info_and_properties(
1✔
350
            TimeInterval::default(),
1✔
351
            TileInformation {
1✔
352
                global_geo_transform: TestDefault::test_default(),
1✔
353
                global_tile_position: [0, 0].into(),
1✔
354
                tile_size_in_pixels,
1✔
355
            },
1✔
356
            0,
357
            raster.into(),
1✔
358
            raster_props,
1✔
359
            CacheHint::default(),
1✔
360
        );
361

362
        let mrs = MockRasterSource {
1✔
363
            params: MockRasterSourceParams {
1✔
364
                data: vec![raster_tile],
1✔
365
                result_descriptor,
1✔
366
            },
1✔
367
        }
1✔
368
        .boxed();
1✔
369

370
        let scaling_mode = ScalingMode::MulSlopeAddOffset;
1✔
371

372
        let output_measurement = None;
1✔
373

374
        let op = RasterScaling {
1✔
375
            params: RasterScalingParams {
1✔
376
                slope: SlopeOffsetSelection::Auto,
1✔
377
                offset: SlopeOffsetSelection::Auto,
1✔
378
                output_measurement,
1✔
379
                scaling_mode,
1✔
380
            },
1✔
381
            sources: SingleRasterSource { raster: mrs },
1✔
382
        }
1✔
383
        .boxed();
1✔
384

385
        let initialized_op = op
1✔
386
            .initialize(WorkflowOperatorPath::initialize_root(), &ctx)
1✔
387
            .await
1✔
388
            .unwrap();
1✔
389

390
        let result_descriptor = initialized_op.result_descriptor();
1✔
391

392
        assert_eq!(result_descriptor.data_type, RasterDataType::U8);
1✔
393
        assert_eq!(
1✔
394
            result_descriptor.bands[0].measurement,
1✔
395
            Measurement::Unitless
396
        );
397

398
        let query_processor = initialized_op.query_processor().unwrap();
1✔
399

400
        let query = geoengine_datatypes::primitives::RasterQueryRectangle::new(
1✔
401
            GridBoundingBox2D::new([0, 0], [1, 1]).unwrap(),
1✔
402
            TimeInterval::default(),
1✔
403
            BandSelection::first(),
1✔
404
        );
405

406
        let TypedRasterQueryProcessor::U8(typed_processor) = query_processor else {
1✔
407
            panic!("expected TypedRasterQueryProcessor::U8");
×
408
        };
409

410
        let stream = typed_processor
1✔
411
            .raster_query(query, &query_ctx)
1✔
412
            .await
1✔
413
            .unwrap();
1✔
414

415
        let results = stream.collect::<Vec<Result<RasterTile2D<u8>>>>().await;
1✔
416

417
        let result_tile = results.as_slice()[0].as_ref().unwrap();
1✔
418

419
        let result_grid = result_tile.grid_array.clone();
1✔
420

421
        match result_grid {
1✔
422
            GridOrEmpty2D::Grid(grid) => {
1✔
423
                assert_eq!(grid.shape(), &GridShape::new([2, 2]));
1✔
424

1✔
425
                let res = grid.masked_element_deref_iterator().collect::<Vec<_>>();
1✔
426

1✔
427
                let expected = vec![Some(15), Some(15), Some(15), Some(13)];
1✔
428

1✔
429
                assert_eq!(res, expected);
1✔
430
            }
1✔
431
            GridOrEmpty2D::Empty(_) => panic!("expected GridOrEmpty2D::Grid"),
1✔
432
        }
1✔
433
    }
1✔
434

435
    #[tokio::test]
436
    async fn test_scale() {
1✔
437
        let tile_size_in_pixels = GridShape2D::new_2d(2, 2);
1✔
438
        let result_descriptor = RasterResultDescriptor {
1✔
439
            data_type: RasterDataType::U8,
1✔
440
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
441
            time: None,
1✔
442
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
443
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
444
                tile_size_in_pixels.bounding_box(),
1✔
445
            ),
1✔
446
            bands: RasterBandDescriptors::new_single_band(),
1✔
447
        };
1✔
448

449
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
450

451
        let raster =
1✔
452
            MaskedGrid2D::from(Grid2D::new(tile_size_in_pixels, vec![15_u8, 15, 15, 13]).unwrap());
1✔
453

454
        let ctx = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
455
        let query_ctx = ctx.mock_query_context(ChunkByteSize::test_default());
1✔
456

457
        let mut raster_props = RasterProperties::default();
1✔
458
        raster_props.set_scale(2.0);
1✔
459
        raster_props.set_offset(1.0);
1✔
460

461
        let raster_tile = RasterTile2D::new_with_tile_info_and_properties(
1✔
462
            TimeInterval::default(),
1✔
463
            TileInformation {
1✔
464
                global_geo_transform: TestDefault::test_default(),
1✔
465
                global_tile_position: [0, 0].into(),
1✔
466
                tile_size_in_pixels,
1✔
467
            },
1✔
468
            0,
469
            raster.into(),
1✔
470
            raster_props,
1✔
471
            CacheHint::default(),
1✔
472
        );
473

474
        let mrs = MockRasterSource {
1✔
475
            params: MockRasterSourceParams {
1✔
476
                data: vec![raster_tile],
1✔
477
                result_descriptor,
1✔
478
            },
1✔
479
        }
1✔
480
        .boxed();
1✔
481

482
        let scaling_mode = ScalingMode::SubOffsetDivSlope;
1✔
483

484
        let output_measurement = None;
1✔
485

486
        let params = RasterScalingParams {
1✔
487
            slope: SlopeOffsetSelection::Auto,
1✔
488
            offset: SlopeOffsetSelection::Auto,
1✔
489
            output_measurement,
1✔
490
            scaling_mode,
1✔
491
        };
1✔
492

493
        let op = RasterScaling {
1✔
494
            params,
1✔
495
            sources: SingleRasterSource { raster: mrs },
1✔
496
        }
1✔
497
        .boxed();
1✔
498

499
        let initialized_op = op
1✔
500
            .initialize(WorkflowOperatorPath::initialize_root(), &ctx)
1✔
501
            .await
1✔
502
            .unwrap();
1✔
503

504
        let result_descriptor = initialized_op.result_descriptor();
1✔
505

506
        assert_eq!(result_descriptor.data_type, RasterDataType::U8);
1✔
507
        assert_eq!(
1✔
508
            result_descriptor.bands[0].measurement,
1✔
509
            Measurement::Unitless
510
        );
511

512
        let query_processor = initialized_op.query_processor().unwrap();
1✔
513

514
        let query = geoengine_datatypes::primitives::RasterQueryRectangle::new(
1✔
515
            GridBoundingBox2D::new([0, 0], [1, 1]).unwrap(),
1✔
516
            TimeInterval::default(),
1✔
517
            BandSelection::first(),
1✔
518
        );
519

520
        let TypedRasterQueryProcessor::U8(typed_processor) = query_processor else {
1✔
521
            panic!("expected TypedRasterQueryProcessor::U8");
×
522
        };
523

524
        let stream = typed_processor
1✔
525
            .raster_query(query, &query_ctx)
1✔
526
            .await
1✔
527
            .unwrap();
1✔
528

529
        let results = stream.collect::<Vec<Result<RasterTile2D<u8>>>>().await;
1✔
530

531
        let result_tile = results.as_slice()[0].as_ref().unwrap();
1✔
532

533
        let result_grid = result_tile.grid_array.clone();
1✔
534

535
        match result_grid {
1✔
536
            GridOrEmpty2D::Grid(grid) => {
1✔
537
                assert_eq!(grid.shape(), &GridShape::new([2, 2]));
1✔
538

1✔
539
                let res = grid.masked_element_deref_iterator().collect::<Vec<_>>();
1✔
540

1✔
541
                let expected = vec![Some(7), Some(7), Some(7), Some(6)];
1✔
542

1✔
543
                assert_eq!(res, expected);
1✔
544
            }
1✔
545
            GridOrEmpty2D::Empty(_) => panic!("expected GridOrEmpty2D::Grid"),
1✔
546
        }
1✔
547
    }
1✔
548
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc