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

geo-engine / geoengine / 18775168616

24 Oct 2025 09:08AM UTC coverage: 88.456%. First build
18775168616

Pull #1084

github

web-flow
Merge c91a1ebdd into 85068105d
Pull Request #1084: feat: Add time_query() and TimeDescriptor

2377 of 2639 new or added lines in 70 files covered. (90.07%)

115295 of 130342 relevant lines covered (88.46%)

225715.35 hits per line

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

94.92
/operators/src/processing/bandwise_expression/mod.rs
1
use std::sync::Arc;
2

3
use crate::engine::{
4
    BoxRasterQueryProcessor, CanonicOperatorName, ExecutionContext, InitializedRasterOperator,
5
    InitializedSources, Operator, OperatorName, QueryContext, QueryProcessor, RasterOperator,
6
    RasterQueryProcessor, RasterResultDescriptor, ResultDescriptor, SingleRasterSource,
7
    TypedRasterQueryProcessor, WorkflowOperatorPath,
8
};
9

10
use crate::util::Result;
11
use async_trait::async_trait;
12
use futures::stream::BoxStream;
13
use futures::{StreamExt, TryStreamExt};
14
use geoengine_datatypes::primitives::{BandSelection, RasterQueryRectangle};
15
use geoengine_datatypes::raster::{
16
    GridBoundingBox2D, GridOrEmpty2D, MapElementsParallel, Pixel, RasterDataType, RasterTile2D,
17
};
18
use geoengine_expression::{
19
    DataType, ExpressionAst, ExpressionParser, LinkedExpression, Parameter,
20
};
21
use serde::{Deserialize, Serialize};
22

23
use super::RasterExpressionError;
24
use super::expression::get_expression_dependencies;
25

26
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27
#[serde(rename_all = "camelCase")]
28
pub struct BandwiseExpressionParams {
29
    pub expression: String,
30
    pub output_type: RasterDataType,
31
    pub map_no_data: bool,
32
    // TODO: new unit for each band?
33
}
34

35
/// This `QueryProcessor` performs a unary expression on all bands of its input raster series.
36
pub type BandwiseExpression = Operator<BandwiseExpressionParams, SingleRasterSource>;
37

38
impl OperatorName for BandwiseExpression {
39
    const TYPE_NAME: &'static str = "BandwiseExpression";
40
}
41

42
#[typetag::serde]
×
43
#[async_trait]
44
impl RasterOperator for BandwiseExpression {
45
    async fn _initialize(
46
        self: Box<Self>,
47
        path: WorkflowOperatorPath,
48
        context: &dyn ExecutionContext,
49
    ) -> Result<Box<dyn InitializedRasterOperator>> {
2✔
50
        let name = CanonicOperatorName::from(&self);
1✔
51

52
        let source = self
1✔
53
            .sources
1✔
54
            .initialize_sources(path.clone(), context)
1✔
55
            .await?
1✔
56
            .raster;
57

58
        let in_descriptor = source.result_descriptor();
1✔
59

60
        // TODO: ensure all bands have same measurement unit?
61

62
        let result_descriptor = in_descriptor.map_data_type(|_| self.params.output_type);
1✔
63

64
        let parameters = vec![Parameter::Number("x".into())];
1✔
65

66
        let expression = ExpressionParser::new(&parameters, DataType::Number)
1✔
67
            .map_err(RasterExpressionError::from)?
1✔
68
            .parse(
1✔
69
                "expression", // TODO: what is the name used for?
1✔
70
                &self.params.expression,
1✔
71
            )
72
            .map_err(RasterExpressionError::from)?;
1✔
73

74
        Ok(Box::new(InitializedBandwiseExpression {
1✔
75
            name,
1✔
76
            path,
1✔
77
            result_descriptor,
1✔
78
            source,
1✔
79
            expression,
1✔
80
            map_no_data: self.params.map_no_data,
1✔
81
        }))
1✔
82
    }
2✔
83

84
    span_fn!(BandwiseExpression);
85
}
86

87
pub struct InitializedBandwiseExpression {
88
    name: CanonicOperatorName,
89
    path: WorkflowOperatorPath,
90
    result_descriptor: RasterResultDescriptor,
91
    source: Box<dyn InitializedRasterOperator>,
92
    expression: ExpressionAst,
93
    map_no_data: bool,
94
}
95

96
impl InitializedRasterOperator for InitializedBandwiseExpression {
97
    fn result_descriptor(&self) -> &RasterResultDescriptor {
1✔
98
        &self.result_descriptor
1✔
99
    }
1✔
100

101
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
1✔
102
        let typed_raster_processor = self.source.query_processor()?.into_f64();
1✔
103

104
        let output_type = self.result_descriptor().data_type;
1✔
105

106
        // TODO: spawn a blocking task for the compilation process
107
        let expression_dependencies = get_expression_dependencies()
1✔
108
            .map_err(|source| RasterExpressionError::Dependencies { source })?;
1✔
109

110
        let expression = LinkedExpression::new(
1✔
111
            self.expression.name(),
1✔
112
            &self.expression.code(),
1✔
113
            expression_dependencies,
1✔
114
        )
115
        .map_err(RasterExpressionError::from)?;
1✔
116

117
        Ok(call_generic_raster_processor!(
×
118
            output_type,
1✔
119
            BandwiseExpressionProcessor::new(
1✔
120
                typed_raster_processor,
1✔
121
                self.result_descriptor.clone(),
1✔
122
                expression,
1✔
123
                self.map_no_data
1✔
124
            )
1✔
125
            .boxed()
1✔
126
        ))
127
    }
1✔
128

129
    fn canonic_name(&self) -> CanonicOperatorName {
×
130
        self.name.clone()
×
131
    }
×
132

133
    fn name(&self) -> &'static str {
×
134
        BandwiseExpression::TYPE_NAME
×
135
    }
×
136

137
    fn path(&self) -> WorkflowOperatorPath {
×
138
        self.path.clone()
×
139
    }
×
140
}
141

142
pub(crate) struct BandwiseExpressionProcessor<TO> {
143
    source: BoxRasterQueryProcessor<f64>,
144
    result_descriptor: RasterResultDescriptor,
145
    expression: Arc<LinkedExpression>,
146
    map_no_data: bool,
147
    phantom: std::marker::PhantomData<TO>,
148
}
149

150
impl<TO> BandwiseExpressionProcessor<TO>
151
where
152
    TO: Pixel,
153
{
154
    pub fn new(
1✔
155
        source: BoxRasterQueryProcessor<f64>,
1✔
156
        result_descriptor: RasterResultDescriptor,
1✔
157
        expression: LinkedExpression,
1✔
158
        map_no_data: bool,
1✔
159
    ) -> Self {
1✔
160
        Self {
1✔
161
            source,
1✔
162
            result_descriptor,
1✔
163
            expression: Arc::new(expression),
1✔
164
            map_no_data,
1✔
165
            phantom: Default::default(),
1✔
166
        }
1✔
167
    }
1✔
168

169
    #[inline]
170
    fn compute_expression(
8✔
171
        raster: RasterTile2D<f64>,
8✔
172
        expression: &LinkedExpression,
8✔
173
        map_no_data: bool,
8✔
174
    ) -> Result<GridOrEmpty2D<TO>> {
8✔
175
        let expression = unsafe {
8✔
176
            // we have to "trust" that the function has the signature we expect
177
            expression
8✔
178
                .function_1::<Option<f64>>()
8✔
179
                .map_err(RasterExpressionError::from)?
8✔
180
        };
181

182
        let map_fn = |in_value: Option<f64>| {
32✔
183
            // TODO: could be a |in_value: T1| if map no data is false!
184
            if !map_no_data && in_value.is_none() {
32✔
185
                return None;
×
186
            }
32✔
187

188
            let result = expression(in_value);
32✔
189

190
            result.map(TO::from_)
32✔
191
        };
32✔
192

193
        let res = raster.grid_array.map_elements_parallel(map_fn);
8✔
194

195
        Result::Ok(res)
8✔
196
    }
8✔
197
}
198

199
#[async_trait]
200
impl<TO> QueryProcessor for BandwiseExpressionProcessor<TO>
201
where
202
    TO: Pixel,
203
{
204
    type Output = RasterTile2D<TO>;
205
    type ResultDescription = RasterResultDescriptor;
206
    type Selection = BandSelection;
207
    type SpatialBounds = GridBoundingBox2D;
208

209
    async fn _query<'a>(
210
        &'a self,
211
        query: RasterQueryRectangle,
212
        ctx: &'a dyn QueryContext,
213
    ) -> Result<BoxStream<'a, Result<RasterTile2D<TO>>>> {
2✔
214
        let stream = self
1✔
215
            .source
1✔
216
            .raster_query(query, ctx)
1✔
217
            .await?
1✔
218
            .and_then(move |tile| async move {
8✔
219
                let expression = self.expression.clone();
8✔
220
                let map_no_data = self.map_no_data;
8✔
221

222
                let time = tile.time;
8✔
223
                let tile_position = tile.tile_position;
8✔
224
                let band = tile.band;
8✔
225
                let global_geo_transform = tile.global_geo_transform;
8✔
226
                let cache_hint = tile.cache_hint;
8✔
227

228
                let out = crate::util::spawn_blocking_with_thread_pool(
8✔
229
                    ctx.thread_pool().clone(),
8✔
230
                    move || Self::compute_expression(tile, &expression, map_no_data),
8✔
231
                )
232
                .await??;
8✔
233

234
                Ok(RasterTile2D::new(
8✔
235
                    time,
8✔
236
                    tile_position,
8✔
237
                    band,
8✔
238
                    global_geo_transform,
8✔
239
                    out,
8✔
240
                    cache_hint,
8✔
241
                ))
8✔
242
            });
16✔
243

244
        Ok(stream.boxed())
1✔
245
    }
2✔
246

247
    fn result_descriptor(&self) -> &Self::ResultDescription {
2✔
248
        &self.result_descriptor
2✔
249
    }
2✔
250
}
251

252
#[async_trait]
253
impl<TO> RasterQueryProcessor for BandwiseExpressionProcessor<TO>
254
where
255
    TO: Pixel,
256
{
257
    type RasterType = TO;
258

259
    async fn time_query<'a>(
260
        &'a self,
261
        query: geoengine_datatypes::primitives::TimeInterval,
262
        ctx: &'a dyn QueryContext,
NEW
263
    ) -> Result<BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>> {
×
NEW
264
        self.source.time_query(query, ctx).await
×
NEW
265
    }
×
266
}
267

268
#[cfg(test)]
269
mod tests {
270
    use geoengine_datatypes::{
271
        primitives::{CacheHint, TimeInterval, TimeStep},
272
        raster::{
273
            Grid, GridBoundingBox2D, GridShape, MapElements, RenameBands,
274
            TilesEqualIgnoringCacheHint,
275
        },
276
        spatial_reference::SpatialReference,
277
        util::test::TestDefault,
278
    };
279

280
    use crate::{
281
        engine::{
282
            MockExecutionContext, MultipleRasterSources, RasterBandDescriptors,
283
            SpatialGridDescriptor, TimeDescriptor,
284
        },
285
        mock::{MockRasterSource, MockRasterSourceParams},
286
        processing::{RasterStacker, RasterStackerParams},
287
    };
288

289
    use super::*;
290

291
    #[tokio::test]
292
    #[allow(clippy::too_many_lines)]
293
    async fn it_computes_bandwise_expression() {
1✔
294
        let data: Vec<RasterTile2D<u8>> = vec![
1✔
295
            RasterTile2D {
1✔
296
                time: TimeInterval::new_unchecked(0, 5),
1✔
297
                tile_position: [-1, 0].into(),
1✔
298
                band: 0,
1✔
299
                global_geo_transform: TestDefault::test_default(),
1✔
300
                grid_array: Grid::new([2, 2].into(), vec![0, 1, 2, 3]).unwrap().into(),
1✔
301
                properties: Default::default(),
1✔
302
                cache_hint: CacheHint::default(),
1✔
303
            },
1✔
304
            RasterTile2D {
1✔
305
                time: TimeInterval::new_unchecked(0, 5),
1✔
306
                tile_position: [-1, 1].into(),
1✔
307
                band: 0,
1✔
308
                global_geo_transform: TestDefault::test_default(),
1✔
309
                grid_array: Grid::new([2, 2].into(), vec![4, 5, 6, 7]).unwrap().into(),
1✔
310
                properties: Default::default(),
1✔
311
                cache_hint: CacheHint::default(),
1✔
312
            },
1✔
313
            RasterTile2D {
1✔
314
                time: TimeInterval::new_unchecked(5, 10),
1✔
315
                tile_position: [-1, 0].into(),
1✔
316
                band: 0,
1✔
317
                global_geo_transform: TestDefault::test_default(),
1✔
318
                grid_array: Grid::new([2, 2].into(), vec![8, 9, 10, 11]).unwrap().into(),
1✔
319
                properties: Default::default(),
1✔
320
                cache_hint: CacheHint::default(),
1✔
321
            },
1✔
322
            RasterTile2D {
1✔
323
                time: TimeInterval::new_unchecked(5, 10),
1✔
324
                tile_position: [-1, 1].into(),
1✔
325
                band: 0,
1✔
326
                global_geo_transform: TestDefault::test_default(),
1✔
327
                grid_array: Grid::new([2, 2].into(), vec![12, 13, 14, 15])
1✔
328
                    .unwrap()
1✔
329
                    .into(),
1✔
330
                properties: Default::default(),
1✔
331
                cache_hint: CacheHint::default(),
1✔
332
            },
1✔
333
        ];
334

335
        let data2: Vec<RasterTile2D<u8>> = vec![
1✔
336
            RasterTile2D {
1✔
337
                time: TimeInterval::new_unchecked(0, 5),
1✔
338
                tile_position: [-1, 0].into(),
1✔
339
                band: 0,
1✔
340
                global_geo_transform: TestDefault::test_default(),
1✔
341
                grid_array: Grid::new([2, 2].into(), vec![16, 17, 18, 19])
1✔
342
                    .unwrap()
1✔
343
                    .into(),
1✔
344
                properties: Default::default(),
1✔
345
                cache_hint: CacheHint::default(),
1✔
346
            },
1✔
347
            RasterTile2D {
1✔
348
                time: TimeInterval::new_unchecked(0, 5),
1✔
349
                tile_position: [-1, 1].into(),
1✔
350
                band: 0,
1✔
351
                global_geo_transform: TestDefault::test_default(),
1✔
352
                grid_array: Grid::new([2, 2].into(), vec![20, 21, 22, 23])
1✔
353
                    .unwrap()
1✔
354
                    .into(),
1✔
355
                properties: Default::default(),
1✔
356
                cache_hint: CacheHint::default(),
1✔
357
            },
1✔
358
            RasterTile2D {
1✔
359
                time: TimeInterval::new_unchecked(5, 10),
1✔
360
                tile_position: [-1, 0].into(),
1✔
361
                band: 0,
1✔
362
                global_geo_transform: TestDefault::test_default(),
1✔
363
                grid_array: Grid::new([2, 2].into(), vec![24, 25, 26, 27])
1✔
364
                    .unwrap()
1✔
365
                    .into(),
1✔
366
                properties: Default::default(),
1✔
367
                cache_hint: CacheHint::default(),
1✔
368
            },
1✔
369
            RasterTile2D {
1✔
370
                time: TimeInterval::new_unchecked(5, 10),
1✔
371
                tile_position: [-1, 1].into(),
1✔
372
                band: 0,
1✔
373
                global_geo_transform: TestDefault::test_default(),
1✔
374
                grid_array: Grid::new([2, 2].into(), vec![28, 29, 30, 31])
1✔
375
                    .unwrap()
1✔
376
                    .into(),
1✔
377
                properties: Default::default(),
1✔
378
                cache_hint: CacheHint::default(),
1✔
379
            },
1✔
380
        ];
381

382
        let result_descriptor = RasterResultDescriptor {
1✔
383
            data_type: RasterDataType::U8,
1✔
384
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
385
            time: TimeDescriptor::new_regular_with_epoch(
1✔
386
                Some(
1✔
387
                    TimeInterval::new(
1✔
388
                        data.first().unwrap().time.start(),
1✔
389
                        data.last().unwrap().time.end(),
1✔
390
                    )
1✔
391
                    .unwrap(),
1✔
392
                ),
1✔
393
                TimeStep::millis(5),
1✔
394
            ),
1✔
395
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
396
                TestDefault::test_default(),
1✔
397
                GridBoundingBox2D::new_min_max(-2, -1, 0, 3).unwrap(),
1✔
398
            ),
1✔
399
            bands: RasterBandDescriptors::new_single_band(),
1✔
400
        };
1✔
401

402
        let mrs1 = MockRasterSource {
1✔
403
            params: MockRasterSourceParams {
1✔
404
                data: data.clone(),
1✔
405
                result_descriptor: result_descriptor.clone(),
1✔
406
            },
1✔
407
        }
1✔
408
        .boxed();
1✔
409

410
        let mrs2 = MockRasterSource {
1✔
411
            params: MockRasterSourceParams {
1✔
412
                data: data2.clone(),
1✔
413
                result_descriptor,
1✔
414
            },
1✔
415
        }
1✔
416
        .boxed();
1✔
417

418
        let stacker = RasterStacker {
1✔
419
            params: RasterStackerParams {
1✔
420
                rename_bands: RenameBands::Default,
1✔
421
            },
1✔
422
            sources: MultipleRasterSources {
1✔
423
                rasters: vec![mrs1, mrs2],
1✔
424
            },
1✔
425
        }
1✔
426
        .boxed();
1✔
427

428
        let expression = BandwiseExpression {
1✔
429
            params: BandwiseExpressionParams {
1✔
430
                expression: "x + 1".to_string(),
1✔
431
                output_type: RasterDataType::U8,
1✔
432
                map_no_data: false,
1✔
433
            },
1✔
434
            sources: SingleRasterSource { raster: stacker },
1✔
435
        }
1✔
436
        .boxed();
1✔
437

438
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
439
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
1✔
440
            shape_array: [2, 2],
1✔
441
        };
1✔
442

443
        let query_rect = RasterQueryRectangle::new(
1✔
444
            GridBoundingBox2D::new_min_max(-2, -1, 0, 3).unwrap(),
1✔
445
            TimeInterval::new_unchecked(0, 10),
1✔
446
            [0, 1].try_into().unwrap(),
1✔
447
        );
448

449
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
450

451
        let op = expression
1✔
452
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
453
            .await
1✔
454
            .unwrap();
1✔
455

456
        let qp = op.query_processor().unwrap().get_u8().unwrap();
1✔
457

458
        let result = qp
1✔
459
            .raster_query(query_rect, &query_ctx)
1✔
460
            .await
1✔
461
            .unwrap()
1✔
462
            .collect::<Vec<_>>()
1✔
463
            .await;
1✔
464
        let result = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
465

466
        let expected: Vec<RasterTile2D<u8>> = data
1✔
467
            .into_iter()
1✔
468
            .zip(data2.into_iter().map(|mut tile| {
4✔
469
                tile.band = 1;
4✔
470
                tile
4✔
471
            }))
4✔
472
            .flat_map(|(a, b)| vec![a.clone(), b.clone()])
4✔
473
            .map(|mut tile| {
8✔
474
                tile.grid_array = tile.grid_array.map_elements(|in_value: u8| in_value + 1);
32✔
475
                tile
8✔
476
            })
8✔
477
            .collect();
1✔
478

479
        assert!(expected.tiles_equal_ignoring_cache_hint(&result));
1✔
480
    }
1✔
481
}
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