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

geo-engine / geoengine / 18909350612

29 Oct 2025 01:22PM UTC coverage: 88.317%. First build
18909350612

Pull #1084

github

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

2409 of 2689 new or added lines in 73 files covered. (89.59%)

115141 of 130373 relevant lines covered (88.32%)

225651.18 hits per line

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

87.84
/operators/src/processing/reprojection.rs
1
use std::marker::PhantomData;
2

3
use super::map_query::MapQueryProcessor;
4
use crate::{
5
    adapters::{
6
        RasterSubQueryAdapter, TileReprojectionSubQuery, TileReprojectionSubqueryGridInfo,
7
        fold_by_coordinate_lookup_future,
8
    },
9
    engine::{
10
        CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources,
11
        InitializedVectorOperator, Operator, OperatorName, QueryContext, QueryProcessor,
12
        RasterOperator, RasterQueryProcessor, RasterResultDescriptor, SingleRasterOrVectorSource,
13
        TypedRasterQueryProcessor, TypedVectorQueryProcessor, VectorOperator, VectorQueryProcessor,
14
        VectorResultDescriptor, WorkflowOperatorPath,
15
    },
16
    error::{self, Error},
17
    util::Result,
18
};
19
use async_trait::async_trait;
20
use futures::stream::BoxStream;
21
use futures::{StreamExt, stream};
22
use geoengine_datatypes::{
23
    collections::FeatureCollection,
24
    operations::reproject::{
25
        CoordinateProjection, CoordinateProjector, Reproject, ReprojectClipped,
26
        reproject_spatial_query,
27
    },
28
    primitives::{
29
        BandSelection, BoundingBox2D, ColumnSelection, Geometry, RasterQueryRectangle,
30
        SpatialPartition2D, VectorQueryRectangle,
31
    },
32
    raster::{GridBoundingBox2D, Pixel, RasterTile2D, TilingSpecification},
33
    spatial_reference::SpatialReference,
34
    util::arrow::ArrowTyped,
35
};
36
use serde::{Deserialize, Serialize};
37

38
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
39
#[serde(rename_all = "camelCase")]
40
pub enum DeriveOutRasterSpecsSource {
41
    DataBounds,
42
    ProjectionBounds,
43
}
44
impl Default for DeriveOutRasterSpecsSource {
45
    fn default() -> Self {
1✔
46
        Self::ProjectionBounds
1✔
47
    }
1✔
48
}
49

50
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
51
#[serde(rename_all = "camelCase")]
52
pub struct ReprojectionParams {
53
    pub target_spatial_reference: SpatialReference,
54
    #[serde(default)]
55
    pub derive_out_spec: DeriveOutRasterSpecsSource,
56
}
57

58
pub type Reprojection = Operator<ReprojectionParams, SingleRasterOrVectorSource>;
59

60
impl Reprojection {}
61

62
impl OperatorName for Reprojection {
63
    const TYPE_NAME: &'static str = "Reprojection";
64
}
65

66
pub struct InitializedVectorReprojection {
67
    name: CanonicOperatorName,
68
    path: WorkflowOperatorPath,
69
    result_descriptor: VectorResultDescriptor,
70
    source: Box<dyn InitializedVectorOperator>,
71
    source_srs: SpatialReference,
72
    target_srs: SpatialReference,
73
}
74

75
pub struct InitializedRasterReprojection<O: InitializedRasterOperator> {
76
    name: CanonicOperatorName,
77
    path: WorkflowOperatorPath,
78
    result_descriptor: RasterResultDescriptor,
79
    source: O,
80
    state: TileReprojectionSubqueryGridInfo,
81
    source_srs: SpatialReference,
82
    target_srs: SpatialReference,
83
}
84

85
impl InitializedVectorReprojection {
86
    /// Create a new `InitializedVectorReprojection` instance.
87
    /// The `source` must have the same `SpatialReference` as `source_srs`.
88
    ///
89
    /// # Errors
90
    /// This function errors if the `source`'s `SpatialReference` is `None`.
91
    /// This function errors if the source's bounding box cannot be reprojected to the target's `SpatialReference`.
92
    pub fn try_new_with_input(
6✔
93
        name: CanonicOperatorName,
6✔
94
        path: WorkflowOperatorPath,
6✔
95
        params: ReprojectionParams,
6✔
96
        source_vector_operator: Box<dyn InitializedVectorOperator>,
6✔
97
    ) -> Result<Self> {
6✔
98
        let in_desc: VectorResultDescriptor = source_vector_operator.result_descriptor().clone();
6✔
99

100
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
101
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
102

103
        let bbox = if let Some(bbox) = in_desc.bbox {
6✔
104
            let projector =
×
105
                CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
×
106

107
            bbox.reproject_clipped(&projector)? // TODO: if this is none then we could skip the whole reprojection similar to raster?
×
108
        } else {
109
            None
6✔
110
        };
111

112
        let out_desc = VectorResultDescriptor {
6✔
113
            spatial_reference: params.target_spatial_reference.into(),
6✔
114
            data_type: in_desc.data_type,
6✔
115
            columns: in_desc.columns.clone(),
6✔
116
            time: in_desc.time,
6✔
117
            bbox,
6✔
118
        };
6✔
119

120
        Ok(InitializedVectorReprojection {
6✔
121
            name,
6✔
122
            path,
6✔
123
            result_descriptor: out_desc,
6✔
124
            source: source_vector_operator,
6✔
125
            source_srs: in_srs,
6✔
126
            target_srs: params.target_spatial_reference,
6✔
127
        })
6✔
128
    }
6✔
129
}
130

131
impl<O: InitializedRasterOperator> InitializedRasterReprojection<O> {
132
    pub fn try_new_with_input(
4✔
133
        name: CanonicOperatorName,
4✔
134
        path: WorkflowOperatorPath,
4✔
135
        params: ReprojectionParams,
4✔
136
        source_raster_operator: O,
4✔
137
        tiling_spec: TilingSpecification,
4✔
138
    ) -> Result<Self> {
4✔
139
        let in_desc: RasterResultDescriptor = source_raster_operator.result_descriptor().clone();
4✔
140

141
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
4✔
142
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
4✔
143

144
        // calculate the intersection of input and output srs in both coordinate systems
145
        let proj_from_to =
4✔
146
            CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
4✔
147

148
        let out_spatial_grid = match params.derive_out_spec {
4✔
149
            DeriveOutRasterSpecsSource::DataBounds => in_desc
1✔
150
                .spatial_grid_descriptor()
1✔
151
                .reproject_clipped(&proj_from_to)?, // TODO: we could skip the intersection (clipped) and try to project larger area
1✔
152
            DeriveOutRasterSpecsSource::ProjectionBounds => {
153
                let in_srs_area: SpatialPartition2D = in_srs.area_of_use_projected()?; // TODO: since we clip in projection anyway, we could use the AOU of the source projection?
3✔
154
                let target_proj_total_grid = in_desc
3✔
155
                    .spatial_grid_descriptor()
3✔
156
                    .spatial_bounds_to_compatible_spatial_grid(in_srs_area)
3✔
157
                    .reproject_clipped(&proj_from_to)?;
3✔
158
                // TODO: we could skip the intersection and try to project larger area
159
                let spatial_bounds_proj =
3✔
160
                    in_desc.spatial_bounds().reproject_clipped(&proj_from_to)?;
3✔
161
                target_proj_total_grid.and_then(|x| {
3✔
162
                    spatial_bounds_proj.map(|spb| x.spatial_bounds_to_compatible_spatial_grid(spb))
3✔
163
                })
3✔
164
            }
165
        };
166

167
        // Operator will return an error when there is no intersection between data and output projection bounds!
168
        let out_spatial_grid = out_spatial_grid.ok_or(error::Error::ReprojectionFailed)?; // TODO: better error!
4✔
169

170
        let out_desc = RasterResultDescriptor {
4✔
171
            spatial_reference: params.target_spatial_reference.into(),
4✔
172
            data_type: in_desc.data_type,
4✔
173
            time: in_desc.time,
4✔
174
            spatial_grid: out_spatial_grid,
4✔
175
            bands: in_desc.bands.clone(),
4✔
176
        };
4✔
177

178
        let state = TileReprojectionSubqueryGridInfo {
4✔
179
            in_spatial_grid: in_desc
4✔
180
                .spatial_grid_descriptor()
4✔
181
                .tiling_grid_definition(tiling_spec)
4✔
182
                .tiling_spatial_grid_definition(),
4✔
183
            out_spatial_grid: out_spatial_grid
4✔
184
                .tiling_grid_definition(tiling_spec)
4✔
185
                .tiling_spatial_grid_definition(),
4✔
186
        };
4✔
187

188
        Ok(InitializedRasterReprojection {
4✔
189
            name,
4✔
190
            path,
4✔
191
            result_descriptor: out_desc,
4✔
192
            source: source_raster_operator,
4✔
193
            state,
4✔
194
            source_srs: in_srs,
4✔
195
            target_srs: params.target_spatial_reference,
4✔
196
        })
4✔
197
    }
4✔
198
}
199

200
#[typetag::serde]
×
201
#[async_trait]
202
impl VectorOperator for Reprojection {
203
    async fn _initialize(
204
        self: Box<Self>,
205
        path: WorkflowOperatorPath,
206
        context: &dyn ExecutionContext,
207
    ) -> Result<Box<dyn InitializedVectorOperator>> {
14✔
208
        let name = CanonicOperatorName::from(&self);
7✔
209

210
        let vector_source =
6✔
211
            self.sources
7✔
212
                .vector()
7✔
213
                .ok_or_else(|| error::Error::InvalidOperatorType {
7✔
214
                    expected: "Vector".to_owned(),
1✔
215
                    found: "Raster".to_owned(),
1✔
216
                })?;
1✔
217

218
        let initialized_source = vector_source
6✔
219
            .initialize_sources(path.clone(), context)
6✔
220
            .await?;
6✔
221

222
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
6✔
223
            name,
6✔
224
            path,
6✔
225
            self.params,
6✔
226
            initialized_source.vector,
6✔
227
        )?;
×
228

229
        Ok(initialized_operator.boxed())
6✔
230
    }
14✔
231

232
    span_fn!(Reprojection);
233
}
234

235
impl InitializedVectorOperator for InitializedVectorReprojection {
236
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
237
        &self.result_descriptor
×
238
    }
×
239

240
    fn query_processor(&self) -> Result<TypedVectorQueryProcessor> {
6✔
241
        let source_srs = self.source_srs;
6✔
242
        let target_srs = self.target_srs;
6✔
243
        match self.source.query_processor()? {
6✔
244
            TypedVectorQueryProcessor::Data(source) => Ok(TypedVectorQueryProcessor::Data(
×
NEW
245
                MapQueryProcessor::new(source, move |query: VectorQueryRectangle| {
×
NEW
246
                    reproject_spatial_query(query.spatial_bounds(), source_srs, target_srs)
×
NEW
247
                        .map(|sqr| {
×
NEW
248
                            sqr.map(|x| {
×
NEW
249
                                VectorQueryRectangle::new(
×
NEW
250
                                    x,
×
NEW
251
                                    query.time_interval(),
×
NEW
252
                                    ColumnSelection::all(),
×
253
                                )
254
                            })
×
NEW
255
                        })
×
NEW
256
                        .map_err(From::from)
×
NEW
257
                })
×
258
                .boxed(),
×
259
            )),
260
            TypedVectorQueryProcessor::MultiPoint(source) => {
4✔
261
                Ok(TypedVectorQueryProcessor::MultiPoint(
4✔
262
                    VectorReprojectionProcessor::new(
4✔
263
                        source,
4✔
264
                        self.result_descriptor.clone(),
4✔
265
                        source_srs,
4✔
266
                        target_srs,
4✔
267
                    )
4✔
268
                    .boxed(),
4✔
269
                ))
4✔
270
            }
271
            TypedVectorQueryProcessor::MultiLineString(source) => {
1✔
272
                Ok(TypedVectorQueryProcessor::MultiLineString(
1✔
273
                    VectorReprojectionProcessor::new(
1✔
274
                        source,
1✔
275
                        self.result_descriptor.clone(),
1✔
276
                        source_srs,
1✔
277
                        target_srs,
1✔
278
                    )
1✔
279
                    .boxed(),
1✔
280
                ))
1✔
281
            }
282
            TypedVectorQueryProcessor::MultiPolygon(source) => {
1✔
283
                Ok(TypedVectorQueryProcessor::MultiPolygon(
1✔
284
                    VectorReprojectionProcessor::new(
1✔
285
                        source,
1✔
286
                        self.result_descriptor.clone(),
1✔
287
                        source_srs,
1✔
288
                        target_srs,
1✔
289
                    )
1✔
290
                    .boxed(),
1✔
291
                ))
1✔
292
            }
293
        }
294
    }
6✔
295

296
    fn canonic_name(&self) -> CanonicOperatorName {
×
297
        self.name.clone()
×
298
    }
×
299

300
    fn name(&self) -> &'static str {
×
301
        Reprojection::TYPE_NAME
×
302
    }
×
303

304
    fn path(&self) -> WorkflowOperatorPath {
×
305
        self.path.clone()
×
306
    }
×
307
}
308

309
struct VectorReprojectionProcessor<Q, G>
310
where
311
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
312
{
313
    source: Q,
314
    result_descriptor: VectorResultDescriptor,
315
    from: SpatialReference,
316
    to: SpatialReference,
317
}
318

319
impl<Q, G> VectorReprojectionProcessor<Q, G>
320
where
321
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
322
{
323
    pub fn new(
6✔
324
        source: Q,
6✔
325
        result_descriptor: VectorResultDescriptor,
6✔
326
        from: SpatialReference,
6✔
327
        to: SpatialReference,
6✔
328
    ) -> Self {
6✔
329
        Self {
6✔
330
            source,
6✔
331
            result_descriptor,
6✔
332
            from,
6✔
333
            to,
6✔
334
        }
6✔
335
    }
6✔
336
}
337

338
#[async_trait]
339
impl<Q, G> QueryProcessor for VectorReprojectionProcessor<Q, G>
340
where
341
    Q: QueryProcessor<
342
            Output = FeatureCollection<G>,
343
            SpatialBounds = BoundingBox2D,
344
            Selection = ColumnSelection,
345
            ResultDescription = VectorResultDescriptor,
346
        >,
347
    FeatureCollection<G>: Reproject<CoordinateProjector, Out = FeatureCollection<G>>,
348
    G: Geometry + ArrowTyped,
349
{
350
    type Output = FeatureCollection<G>;
351
    type SpatialBounds = BoundingBox2D;
352
    type Selection = ColumnSelection;
353
    type ResultDescription = VectorResultDescriptor;
354

355
    async fn _query<'a>(
356
        &'a self,
357
        query: VectorQueryRectangle,
358
        ctx: &'a dyn QueryContext,
359
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
12✔
360
        let rewritten_spatial_query =
6✔
361
            reproject_spatial_query(query.spatial_bounds(), self.from, self.to)?;
6✔
362

363
        let rewritten_query = rewritten_spatial_query.map(|rwq| query.select_spatial_bounds(rwq));
6✔
364

365
        if let Some(rewritten_query) = rewritten_query {
6✔
366
            Ok(self
5✔
367
                .source
5✔
368
                .query(rewritten_query, ctx)
5✔
369
                .await?
5✔
370
                .map(move |collection_result| {
5✔
371
                    collection_result.and_then(|collection| {
5✔
372
                        CoordinateProjector::from_known_srs(self.from, self.to)
5✔
373
                            .and_then(|projector| collection.reproject(projector.as_ref()))
5✔
374
                            .map_err(Into::into)
5✔
375
                    })
5✔
376
                })
5✔
377
                .boxed())
5✔
378
        } else {
379
            let res = Ok(FeatureCollection::empty());
1✔
380
            Ok(Box::pin(stream::once(async { res })))
1✔
381
        }
382
    }
12✔
383

384
    fn result_descriptor(&self) -> &VectorResultDescriptor {
12✔
385
        &self.result_descriptor
12✔
386
    }
12✔
387
}
388

389
#[typetag::serde]
×
390
#[async_trait]
391
impl RasterOperator for Reprojection {
392
    async fn _initialize(
393
        self: Box<Self>,
394
        path: WorkflowOperatorPath,
395
        context: &dyn ExecutionContext,
396
    ) -> Result<Box<dyn InitializedRasterOperator>> {
8✔
397
        let name = CanonicOperatorName::from(&self);
4✔
398

399
        let raster_source =
4✔
400
            self.sources
4✔
401
                .raster()
4✔
402
                .ok_or_else(|| error::Error::InvalidOperatorType {
4✔
403
                    expected: "Raster".to_owned(),
×
404
                    found: "Vector".to_owned(),
×
405
                })?;
×
406

407
        let initialized_source = raster_source
4✔
408
            .initialize_sources(path.clone(), context)
4✔
409
            .await?;
4✔
410

411
        let initialized_operator = InitializedRasterReprojection::try_new_with_input(
4✔
412
            name,
4✔
413
            path,
4✔
414
            self.params,
4✔
415
            initialized_source.raster,
4✔
416
            context.tiling_specification(),
4✔
417
        )?;
×
418

419
        Ok(initialized_operator.boxed())
4✔
420
    }
8✔
421

422
    span_fn!(Reprojection);
423
}
424

425
impl<O: InitializedRasterOperator> InitializedRasterOperator for InitializedRasterReprojection<O> {
426
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
427
        &self.result_descriptor
×
428
    }
×
429

430
    // i know there is a macro somewhere. we need to re-work this when we have the no-data value anyway.
431
    #[allow(clippy::too_many_lines)]
432
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
4✔
433
        let q = self.source.query_processor()?;
4✔
434

435
        Ok(match self.result_descriptor.data_type {
4✔
436
            geoengine_datatypes::raster::RasterDataType::U8 => {
437
                let qt = q
4✔
438
                    .get_u8()
4✔
439
                    .expect("the result descriptor and query processor type should match");
4✔
440
                TypedRasterQueryProcessor::U8(Box::new(RasterReprojectionProcessor::new(
4✔
441
                    qt,
4✔
442
                    self.result_descriptor.clone(),
4✔
443
                    self.source_srs,
4✔
444
                    self.target_srs,
4✔
445
                    self.state,
4✔
446
                )))
4✔
447
            }
448
            geoengine_datatypes::raster::RasterDataType::U16 => {
449
                let qt = q
×
450
                    .get_u16()
×
451
                    .expect("the result descriptor and query processor type should match");
×
452
                TypedRasterQueryProcessor::U16(Box::new(RasterReprojectionProcessor::new(
×
453
                    qt,
×
454
                    self.result_descriptor.clone(),
×
455
                    self.source_srs,
×
456
                    self.target_srs,
×
457
                    self.state,
×
458
                )))
×
459
            }
460

461
            geoengine_datatypes::raster::RasterDataType::U32 => {
462
                let qt = q
×
463
                    .get_u32()
×
464
                    .expect("the result descriptor and query processor type should match");
×
465
                TypedRasterQueryProcessor::U32(Box::new(RasterReprojectionProcessor::new(
×
466
                    qt,
×
467
                    self.result_descriptor.clone(),
×
468
                    self.source_srs,
×
469
                    self.target_srs,
×
470
                    self.state,
×
471
                )))
×
472
            }
473
            geoengine_datatypes::raster::RasterDataType::U64 => {
474
                let qt = q
×
475
                    .get_u64()
×
476
                    .expect("the result descriptor and query processor type should match");
×
477
                TypedRasterQueryProcessor::U64(Box::new(RasterReprojectionProcessor::new(
×
478
                    qt,
×
479
                    self.result_descriptor.clone(),
×
480
                    self.source_srs,
×
481
                    self.target_srs,
×
482
                    self.state,
×
483
                )))
×
484
            }
485
            geoengine_datatypes::raster::RasterDataType::I8 => {
486
                let qt = q
×
487
                    .get_i8()
×
488
                    .expect("the result descriptor and query processor type should match");
×
489
                TypedRasterQueryProcessor::I8(Box::new(RasterReprojectionProcessor::new(
×
490
                    qt,
×
491
                    self.result_descriptor.clone(),
×
492
                    self.source_srs,
×
493
                    self.target_srs,
×
494
                    self.state,
×
495
                )))
×
496
            }
497
            geoengine_datatypes::raster::RasterDataType::I16 => {
498
                let qt = q
×
499
                    .get_i16()
×
500
                    .expect("the result descriptor and query processor type should match");
×
501
                TypedRasterQueryProcessor::I16(Box::new(RasterReprojectionProcessor::new(
×
502
                    qt,
×
503
                    self.result_descriptor.clone(),
×
504
                    self.source_srs,
×
505
                    self.target_srs,
×
506
                    self.state,
×
507
                )))
×
508
            }
509
            geoengine_datatypes::raster::RasterDataType::I32 => {
510
                let qt = q
×
511
                    .get_i32()
×
512
                    .expect("the result descriptor and query processor type should match");
×
513
                TypedRasterQueryProcessor::I32(Box::new(RasterReprojectionProcessor::new(
×
514
                    qt,
×
515
                    self.result_descriptor.clone(),
×
516
                    self.source_srs,
×
517
                    self.target_srs,
×
518
                    self.state,
×
519
                )))
×
520
            }
521
            geoengine_datatypes::raster::RasterDataType::I64 => {
522
                let qt = q
×
523
                    .get_i64()
×
524
                    .expect("the result descriptor and query processor type should match");
×
525
                TypedRasterQueryProcessor::I64(Box::new(RasterReprojectionProcessor::new(
×
526
                    qt,
×
527
                    self.result_descriptor.clone(),
×
528
                    self.source_srs,
×
529
                    self.target_srs,
×
530
                    self.state,
×
531
                )))
×
532
            }
533
            geoengine_datatypes::raster::RasterDataType::F32 => {
534
                let qt = q
×
535
                    .get_f32()
×
536
                    .expect("the result descriptor and query processor type should match");
×
537
                TypedRasterQueryProcessor::F32(Box::new(RasterReprojectionProcessor::new(
×
538
                    qt,
×
539
                    self.result_descriptor.clone(),
×
540
                    self.source_srs,
×
541
                    self.target_srs,
×
542
                    self.state,
×
543
                )))
×
544
            }
545
            geoengine_datatypes::raster::RasterDataType::F64 => {
546
                let qt = q
×
547
                    .get_f64()
×
548
                    .expect("the result descriptor and query processor type should match");
×
549
                TypedRasterQueryProcessor::F64(Box::new(RasterReprojectionProcessor::new(
×
550
                    qt,
×
551
                    self.result_descriptor.clone(),
×
552
                    self.source_srs,
×
553
                    self.target_srs,
×
554
                    self.state,
×
555
                )))
×
556
            }
557
        })
558
    }
4✔
559

560
    fn canonic_name(&self) -> CanonicOperatorName {
×
561
        self.name.clone()
×
562
    }
×
563

564
    fn name(&self) -> &'static str {
×
565
        Reprojection::TYPE_NAME
×
566
    }
×
567

568
    fn path(&self) -> WorkflowOperatorPath {
×
569
        self.path.clone()
×
570
    }
×
571
}
572

573
pub struct RasterReprojectionProcessor<Q, P>
574
where
575
    Q: RasterQueryProcessor<RasterType = P>,
576
{
577
    source: Q,
578
    result_descriptor: RasterResultDescriptor,
579
    from: SpatialReference,
580
    to: SpatialReference,
581
    state: TileReprojectionSubqueryGridInfo,
582
    _phantom_data: PhantomData<P>,
583
}
584

585
impl<Q, P> RasterReprojectionProcessor<Q, P>
586
where
587
    Q: RasterQueryProcessor<RasterType = P>,
588
    P: Pixel,
589
{
590
    pub fn new(
4✔
591
        source: Q,
4✔
592
        result_descriptor: RasterResultDescriptor,
4✔
593
        from: SpatialReference,
4✔
594
        to: SpatialReference,
4✔
595
        state: TileReprojectionSubqueryGridInfo,
4✔
596
    ) -> Self {
4✔
597
        Self {
4✔
598
            source,
4✔
599
            result_descriptor,
4✔
600
            from,
4✔
601
            to,
4✔
602
            state,
4✔
603
            _phantom_data: PhantomData,
4✔
604
        }
4✔
605
    }
4✔
606
}
607

608
#[async_trait]
609
impl<Q, P> QueryProcessor for RasterReprojectionProcessor<Q, P>
610
where
611
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
612
    P: Pixel,
613
{
614
    type Output = RasterTile2D<P>;
615
    type SpatialBounds = GridBoundingBox2D;
616
    type Selection = BandSelection;
617
    type ResultDescription = RasterResultDescriptor;
618

619
    async fn _query<'a>(
620
        &'a self,
621
        query: RasterQueryRectangle,
622
        ctx: &'a dyn QueryContext,
623
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
8✔
624
        let state = self.state;
4✔
625

626
        // setup the subquery
627
        let sub_query_spec = TileReprojectionSubQuery {
4✔
628
            in_srs: self.from,
4✔
629
            out_srs: self.to,
4✔
630
            fold_fn: fold_by_coordinate_lookup_future,
4✔
631
            state,
4✔
632
            _phantom_data: PhantomData,
4✔
633
        };
4✔
634

635
        let tiling_strat = self
4✔
636
            .result_descriptor
4✔
637
            .spatial_grid_descriptor()
4✔
638
            .tiling_grid_definition(ctx.tiling_specification())
4✔
639
            .generate_data_tiling_strategy();
4✔
640

641
        let time_stream = self.time_query(query.time_interval(), ctx).await?;
4✔
642

643
        // return the adapter which will reproject the tiles and uses the fill adapter to inject missing tiles
644
        Ok(RasterSubQueryAdapter::<'a, P, _, _, _>::new(
4✔
645
            &self.source,
4✔
646
            query,
4✔
647
            tiling_strat,
4✔
648
            ctx,
4✔
649
            sub_query_spec,
4✔
650
            time_stream,
4✔
651
        )
4✔
652
        .box_pin())
4✔
653
    }
8✔
654

655
    fn result_descriptor(&self) -> &RasterResultDescriptor {
10✔
656
        &self.result_descriptor
10✔
657
    }
10✔
658
}
659

660
#[async_trait]
661
impl<Q, P> RasterQueryProcessor for RasterReprojectionProcessor<Q, P>
662
where
663
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
664
    P: Pixel,
665
{
666
    type RasterType = P;
667

668
    async fn time_query<'a>(
669
        &'a self,
670
        query: geoengine_datatypes::primitives::TimeInterval,
671
        ctx: &'a dyn QueryContext,
672
    ) -> Result<BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>> {
8✔
673
        self.source.time_query(query, ctx).await
4✔
674
    }
8✔
675
}
676

677
#[cfg(test)]
678
mod tests {
679
    use super::*;
680
    use crate::engine::{MockExecutionContext, RasterBandDescriptors, SpatialGridDescriptor};
681
    use crate::mock::MockFeatureCollectionSource;
682
    use crate::mock::{MockRasterSource, MockRasterSourceParams};
683
    use crate::source::{
684
        FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters, GdalMetaDataRegular,
685
        GdalMetaDataStatic, GdalSourceTimePlaceholder, TimeReference,
686
    };
687
    use crate::util::gdal::add_ndvi_dataset;
688
    use crate::{
689
        engine::{ChunkByteSize, VectorOperator},
690
        source::{GdalSource, GdalSourceParameters},
691
        test_data,
692
    };
693
    use float_cmp::{approx_eq, assert_approx_eq};
694
    use futures::StreamExt;
695
    use geoengine_datatypes::collections::IntoGeometryIterator;
696
    use geoengine_datatypes::dataset::{DataId, DatasetId, NamedData};
697
    use geoengine_datatypes::hashmap;
698
    use geoengine_datatypes::primitives::{
699
        CacheHint, CacheTtlSeconds, DateTimeParseFormat, SpatialResolution, TimeGranularity,
700
        TimeInstance,
701
    };
702
    use geoengine_datatypes::primitives::{Coordinate2D, TimeStep};
703
    use geoengine_datatypes::raster::{
704
        GeoTransform, GridBoundingBox2D, GridShape2D, GridSize, SpatialGridDefinition,
705
        TilesEqualIgnoringCacheHint,
706
    };
707
    use geoengine_datatypes::{
708
        collections::{
709
            GeometryCollection, MultiLineStringCollection, MultiPointCollection,
710
            MultiPolygonCollection,
711
        },
712
        primitives::{BoundingBox2D, MultiLineString, MultiPoint, MultiPolygon, TimeInterval},
713
        raster::{Grid, RasterDataType, RasterTile2D},
714
        spatial_reference::SpatialReferenceAuthority,
715
        util::{
716
            Identifier,
717
            test::TestDefault,
718
            well_known_data::{
719
                COLOGNE_EPSG_900_913, COLOGNE_EPSG_4326, HAMBURG_EPSG_900_913, HAMBURG_EPSG_4326,
720
                MARBURG_EPSG_900_913, MARBURG_EPSG_4326,
721
            },
722
        },
723
    };
724
    use std::collections::HashMap;
725
    use std::path::PathBuf;
726
    use std::str::FromStr;
727

728
    #[tokio::test]
729
    async fn multi_point() -> Result<()> {
1✔
730
        let points = MultiPointCollection::from_data(
1✔
731
            MultiPoint::many(vec![
1✔
732
                MARBURG_EPSG_4326,
733
                COLOGNE_EPSG_4326,
734
                HAMBURG_EPSG_4326,
735
            ])
736
            .unwrap(),
1✔
737
            vec![TimeInterval::new_unchecked(0, 1); 3],
1✔
738
            Default::default(),
1✔
739
            CacheHint::default(),
1✔
740
        )?;
×
741

742
        let expected = MultiPoint::many(vec![
1✔
743
            MARBURG_EPSG_900_913,
744
            COLOGNE_EPSG_900_913,
745
            HAMBURG_EPSG_900_913,
746
        ])
747
        .unwrap();
1✔
748

749
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
750

751
        let target_spatial_reference =
1✔
752
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
753

754
        let exe_ctx = MockExecutionContext::test_default();
1✔
755

756
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
757
            params: ReprojectionParams {
1✔
758
                target_spatial_reference,
1✔
759
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
760
            },
1✔
761
            sources: SingleRasterOrVectorSource {
1✔
762
                source: point_source.into(),
1✔
763
            },
1✔
764
        })
1✔
765
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
766
        .await?;
1✔
767

768
        let query_processor = initialized_operator.query_processor()?;
1✔
769

770
        let query_processor = query_processor.multi_point().unwrap();
1✔
771

772
        let query_rectangle = VectorQueryRectangle::new(
1✔
773
            BoundingBox2D::new(
1✔
774
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
775
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
776
            )
777
            .unwrap(),
1✔
778
            TimeInterval::default(),
1✔
779
            ColumnSelection::all(),
1✔
780
        );
781
        let ctx = exe_ctx.mock_query_context(ChunkByteSize::MAX);
1✔
782

783
        let query = query_processor.query(query_rectangle, &ctx).await.unwrap();
1✔
784

785
        let result = query
1✔
786
            .map(Result::unwrap)
1✔
787
            .collect::<Vec<MultiPointCollection>>()
1✔
788
            .await;
1✔
789

790
        assert_eq!(result.len(), 1);
1✔
791

792
        result[0]
1✔
793
            .geometries()
1✔
794
            .zip(expected.iter())
1✔
795
            .for_each(|(a, e)| {
3✔
796
                assert!(approx_eq!(&MultiPoint, &a.into(), e, epsilon = 0.00001));
3✔
797
            });
3✔
798

799
        Ok(())
2✔
800
    }
1✔
801

802
    #[tokio::test]
803
    async fn multi_lines() -> Result<()> {
1✔
804
        let lines = MultiLineStringCollection::from_data(
1✔
805
            vec![
1✔
806
                MultiLineString::new(vec![vec![
1✔
807
                    MARBURG_EPSG_4326,
808
                    COLOGNE_EPSG_4326,
809
                    HAMBURG_EPSG_4326,
810
                ]])
811
                .unwrap(),
1✔
812
            ],
813
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
814
            Default::default(),
1✔
815
            CacheHint::default(),
1✔
816
        )?;
×
817

818
        let expected = [MultiLineString::new(vec![vec![
1✔
819
            MARBURG_EPSG_900_913,
1✔
820
            COLOGNE_EPSG_900_913,
1✔
821
            HAMBURG_EPSG_900_913,
1✔
822
        ]])
1✔
823
        .unwrap()];
1✔
824

825
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
826

827
        let target_spatial_reference =
1✔
828
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
829

830
        let exe_ctx = MockExecutionContext::test_default();
1✔
831

832
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
833
            params: ReprojectionParams {
1✔
834
                target_spatial_reference,
1✔
835
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
836
            },
1✔
837
            sources: SingleRasterOrVectorSource {
1✔
838
                source: lines_source.into(),
1✔
839
            },
1✔
840
        })
1✔
841
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
842
        .await?;
1✔
843

844
        let query_processor = initialized_operator.query_processor()?;
1✔
845

846
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
847

848
        let query_rectangle = VectorQueryRectangle::new(
1✔
849
            BoundingBox2D::new(
1✔
850
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
851
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
852
            )
853
            .unwrap(),
1✔
854
            TimeInterval::default(),
1✔
855
            ColumnSelection::all(),
1✔
856
        );
857
        let ctx = exe_ctx.mock_query_context(ChunkByteSize::MAX);
1✔
858

859
        let query = query_processor.query(query_rectangle, &ctx).await.unwrap();
1✔
860

861
        let result = query
1✔
862
            .map(Result::unwrap)
1✔
863
            .collect::<Vec<MultiLineStringCollection>>()
1✔
864
            .await;
1✔
865

866
        assert_eq!(result.len(), 1);
1✔
867

868
        result[0]
1✔
869
            .geometries()
1✔
870
            .zip(expected.iter())
1✔
871
            .for_each(|(a, e)| {
1✔
872
                assert!(approx_eq!(
1✔
873
                    &MultiLineString,
874
                    &a.into(),
1✔
875
                    e,
1✔
876
                    epsilon = 0.00001
877
                ));
878
            });
1✔
879

880
        Ok(())
2✔
881
    }
1✔
882

883
    #[tokio::test]
884
    async fn multi_polygons() -> Result<()> {
1✔
885
        let polygons = MultiPolygonCollection::from_data(
1✔
886
            vec![
1✔
887
                MultiPolygon::new(vec![vec![vec![
1✔
888
                    MARBURG_EPSG_4326,
889
                    COLOGNE_EPSG_4326,
890
                    HAMBURG_EPSG_4326,
891
                    MARBURG_EPSG_4326,
892
                ]]])
893
                .unwrap(),
1✔
894
            ],
895
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
896
            Default::default(),
1✔
897
            CacheHint::default(),
1✔
898
        )?;
×
899

900
        let expected = [MultiPolygon::new(vec![vec![vec![
1✔
901
            MARBURG_EPSG_900_913,
1✔
902
            COLOGNE_EPSG_900_913,
1✔
903
            HAMBURG_EPSG_900_913,
1✔
904
            MARBURG_EPSG_900_913,
1✔
905
        ]]])
1✔
906
        .unwrap()];
1✔
907

908
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
909

910
        let target_spatial_reference =
1✔
911
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
912

913
        let exe_ctx = MockExecutionContext::test_default();
1✔
914

915
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
916
            params: ReprojectionParams {
1✔
917
                target_spatial_reference,
1✔
918
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
919
            },
1✔
920
            sources: SingleRasterOrVectorSource {
1✔
921
                source: polygon_source.into(),
1✔
922
            },
1✔
923
        })
1✔
924
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
925
        .await?;
1✔
926

927
        let query_processor = initialized_operator.query_processor()?;
1✔
928

929
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
930

931
        let query_rectangle = VectorQueryRectangle::new(
1✔
932
            BoundingBox2D::new(
1✔
933
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
934
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
935
            )
936
            .unwrap(),
1✔
937
            TimeInterval::default(),
1✔
938
            ColumnSelection::all(),
1✔
939
        );
940
        let ctx = exe_ctx.mock_query_context(ChunkByteSize::MAX);
1✔
941

942
        let query = query_processor.query(query_rectangle, &ctx).await.unwrap();
1✔
943

944
        let result = query
1✔
945
            .map(Result::unwrap)
1✔
946
            .collect::<Vec<MultiPolygonCollection>>()
1✔
947
            .await;
1✔
948

949
        assert_eq!(result.len(), 1);
1✔
950

951
        result[0]
1✔
952
            .geometries()
1✔
953
            .zip(expected.iter())
1✔
954
            .for_each(|(a, e)| {
1✔
955
                assert!(approx_eq!(&MultiPolygon, &a.into(), e, epsilon = 0.00001));
1✔
956
            });
1✔
957

958
        Ok(())
2✔
959
    }
1✔
960

961
    #[allow(clippy::too_many_lines)]
962
    #[tokio::test]
963
    async fn raster_identity() -> Result<()> {
1✔
964
        let projection = SpatialReference::epsg_4326();
1✔
965

966
        let data = vec![
1✔
967
            RasterTile2D {
1✔
968
                time: TimeInterval::new_unchecked(0, 5),
1✔
969
                tile_position: [-1, 0].into(),
1✔
970
                band: 0,
1✔
971
                global_geo_transform: TestDefault::test_default(),
1✔
972
                grid_array: Grid::new([2, 2].into(), vec![1_u8, 2, 3, 4])
1✔
973
                    .unwrap()
1✔
974
                    .into(),
1✔
975
                properties: Default::default(),
1✔
976
                cache_hint: CacheHint::default(),
1✔
977
            },
1✔
978
            RasterTile2D {
1✔
979
                time: TimeInterval::new_unchecked(0, 5),
1✔
980
                tile_position: [-1, 1].into(),
1✔
981
                band: 0,
1✔
982
                global_geo_transform: TestDefault::test_default(),
1✔
983
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
984
                properties: Default::default(),
1✔
985
                cache_hint: CacheHint::default(),
1✔
986
            },
1✔
987
            RasterTile2D {
1✔
988
                time: TimeInterval::new_unchecked(0, 5),
1✔
989
                tile_position: [0, 0].into(),
1✔
990
                band: 0,
1✔
991
                global_geo_transform: TestDefault::test_default(),
1✔
992
                grid_array: Grid::new([2, 2].into(), vec![1_u8, 2, 3, 4])
1✔
993
                    .unwrap()
1✔
994
                    .into(),
1✔
995
                properties: Default::default(),
1✔
996
                cache_hint: CacheHint::default(),
1✔
997
            },
1✔
998
            RasterTile2D {
1✔
999
                time: TimeInterval::new_unchecked(0, 5),
1✔
1000
                tile_position: [0, 1].into(),
1✔
1001
                band: 0,
1✔
1002
                global_geo_transform: TestDefault::test_default(),
1✔
1003
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
1004
                properties: Default::default(),
1✔
1005
                cache_hint: CacheHint::default(),
1✔
1006
            },
1✔
1007
            RasterTile2D {
1✔
1008
                time: TimeInterval::new_unchecked(5, 10),
1✔
1009
                tile_position: [-1, 0].into(),
1✔
1010
                band: 0,
1✔
1011
                global_geo_transform: TestDefault::test_default(),
1✔
1012
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
1013
                    .unwrap()
1✔
1014
                    .into(),
1✔
1015
                properties: Default::default(),
1✔
1016
                cache_hint: CacheHint::default(),
1✔
1017
            },
1✔
1018
            RasterTile2D {
1✔
1019
                time: TimeInterval::new_unchecked(5, 10),
1✔
1020
                tile_position: [-1, 1].into(),
1✔
1021
                band: 0,
1✔
1022
                global_geo_transform: TestDefault::test_default(),
1✔
1023
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
1024
                    .unwrap()
1✔
1025
                    .into(),
1✔
1026
                properties: Default::default(),
1✔
1027
                cache_hint: CacheHint::default(),
1✔
1028
            },
1✔
1029
            RasterTile2D {
1✔
1030
                time: TimeInterval::new_unchecked(5, 10),
1✔
1031
                tile_position: [0, 0].into(),
1✔
1032
                band: 0,
1✔
1033
                global_geo_transform: TestDefault::test_default(),
1✔
1034
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
1035
                    .unwrap()
1✔
1036
                    .into(),
1✔
1037
                properties: Default::default(),
1✔
1038
                cache_hint: CacheHint::default(),
1✔
1039
            },
1✔
1040
            RasterTile2D {
1✔
1041
                time: TimeInterval::new_unchecked(5, 10),
1✔
1042
                tile_position: [0, 1].into(),
1✔
1043
                band: 0,
1✔
1044
                global_geo_transform: TestDefault::test_default(),
1✔
1045
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
1046
                    .unwrap()
1✔
1047
                    .into(),
1✔
1048
                properties: Default::default(),
1✔
1049
                cache_hint: CacheHint::default(),
1✔
1050
            },
1✔
1051
        ];
1052

1053
        let geo_transform = GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.);
1✔
1054

1055
        let result_descriptor = RasterResultDescriptor {
1✔
1056
            data_type: RasterDataType::U8,
1✔
1057
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1058
            time: crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
1059
                Some(TimeInterval::new_unchecked(0, 10)),
1✔
1060
                TimeStep::millis(5),
1✔
1061
            ),
1✔
1062
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1063
                geo_transform,
1✔
1064
                GridBoundingBox2D::new([-2, 0], [1, 3]).unwrap(),
1✔
1065
            ),
1✔
1066
            bands: RasterBandDescriptors::new_single_band(),
1✔
1067
        };
1✔
1068

1069
        let exe_ctx =
1✔
1070
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
1071

1072
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1073

1074
        let mrs1 = MockRasterSource {
1✔
1075
            params: MockRasterSourceParams {
1✔
1076
                data: data.clone(),
1✔
1077
                result_descriptor,
1✔
1078
            },
1✔
1079
        }
1✔
1080
        .boxed();
1✔
1081

1082
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1083
            params: ReprojectionParams {
1✔
1084
                target_spatial_reference: projection, // This test will do a identity reprojection
1✔
1085
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1086
            },
1✔
1087
            sources: SingleRasterOrVectorSource {
1✔
1088
                source: mrs1.into(),
1✔
1089
            },
1✔
1090
        })
1✔
1091
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1092
        .await?;
1✔
1093

1094
        let qp = initialized_operator
1✔
1095
            .query_processor()
1✔
1096
            .unwrap()
1✔
1097
            .get_u8()
1✔
1098
            .unwrap();
1✔
1099

1100
        let query_rect = RasterQueryRectangle::new(
1✔
1101
            GridBoundingBox2D::new([-2, 0], [1, 3]).unwrap(),
1✔
1102
            TimeInterval::new_unchecked(0, 10),
1✔
1103
            BandSelection::first(),
1✔
1104
        );
1105

1106
        let a = qp.raster_query(query_rect, &query_ctx).await?;
1✔
1107

1108
        let res = a
1✔
1109
            .map(Result::unwrap)
1✔
1110
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1111
            .await;
1✔
1112
        assert!(data.tiles_equal_ignoring_cache_hint(&res));
1✔
1113

1114
        Ok(())
2✔
1115
    }
1✔
1116

1117
    #[tokio::test]
1118
    async fn raster_ndvi_3857() -> Result<()> {
1✔
1119
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1120
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1121

1122
        let tile_size = GridShape2D::new_2d(512, 512);
1✔
1123
        exe_ctx.tiling_specification = TilingSpecification::new(tile_size);
1✔
1124

1125
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1126

1127
        let time_interval = TimeInterval::new_unchecked(1_396_303_200_000, 1_396_389_600_000);
1✔
1128
        // 2014-04-01
1129

1130
        let gdal_op = GdalSource {
1✔
1131
            params: GdalSourceParameters::new(id.clone()),
1✔
1132
        }
1✔
1133
        .boxed();
1✔
1134

1135
        let projection = SpatialReference::new(
1✔
1136
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
1137
            3857,
1138
        );
1139

1140
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1141
            params: ReprojectionParams {
1✔
1142
                target_spatial_reference: projection,
1✔
1143
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1144
            },
1✔
1145
            sources: SingleRasterOrVectorSource {
1✔
1146
                source: gdal_op.into(),
1✔
1147
            },
1✔
1148
        })
1✔
1149
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1150
        .await?;
1✔
1151

1152
        let qp = initialized_operator
1✔
1153
            .query_processor()
1✔
1154
            .unwrap()
1✔
1155
            .get_u8()
1✔
1156
            .unwrap();
1✔
1157

1158
        let result_descritptor = qp.result_descriptor();
1✔
1159

1160
        assert_approx_eq!(
1✔
1161
            f64,
1162
            14_255.015_508_816_849, // TODO: GDAL output is 14228.560819126376373
1163
            result_descritptor
1✔
1164
                .spatial_grid_descriptor()
1✔
1165
                .spatial_resolution()
1✔
1166
                .x,
1167
            epsilon = 0.000_001
1168
        );
1169

1170
        assert_approx_eq!(
1✔
1171
            f64,
1172
            14_255.015_508_816_849, // TODO: GDAL output is -14233.615370039031404
1173
            result_descritptor
1✔
1174
                .spatial_grid_descriptor()
1✔
1175
                .spatial_resolution()
1✔
1176
                .y,
1177
            epsilon = 0.000_001
1178
        );
1179

1180
        let tlz = result_descritptor
1✔
1181
            .spatial_grid_descriptor()
1✔
1182
            .tiling_grid_definition(query_ctx.tiling_specification())
1✔
1183
            .generate_data_tiling_strategy();
1✔
1184
        let query_tl_pixel = tlz.tile_idx_to_global_pixel_idx([-1, 0].into());
1✔
1185
        let query_bounds =
1✔
1186
            GridBoundingBox2D::new(query_tl_pixel, query_tl_pixel + [511, 511]).unwrap();
1✔
1187

1188
        let qrect = RasterQueryRectangle::new(query_bounds, time_interval, BandSelection::first());
1✔
1189

1190
        let qs = qp.raster_query(qrect.clone(), &query_ctx).await.unwrap();
1✔
1191

1192
        let res = qs
1✔
1193
            .map(Result::unwrap)
1✔
1194
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1195
            .await;
1✔
1196

1197
        // get the worldfile
1198
        // println!("{}", res[0].tile_geo_transform().worldfile_string());
1199

1200
        // Write the tile to a file
1201

1202
        /*
1203
        let mut buffer = std::fs::File::create("MOD13A2_M_NDVI_2014-04-01_tile-20_v6.rst")?;
1204

1205
        std::io::Write::write(
1206
            &mut buffer,
1207
            res[0]
1208
                .clone()
1209
                .into_materialized_tile()
1210
                .grid_array
1211
                .inner_grid
1212
                .data
1213
                .as_slice(),
1214
        )?;
1215
        */
1216

1217
        // This check is against a tile produced by the operator itself. It was visually validated. TODO: rebuild when open issues are solved.
1218
        // A perfect validation would be against a GDAL output generated like this:
1219
        // gdalwarp -t_srs EPSG:3857 -r near -te_srs EPSG:3857 -of GTiff ./MOD13A2_M_NDVI_2014-04-01.TIFF ./MOD13A2_M_NDVI_2014-04-01.TIFF
1220

1221
        assert_eq!(
1✔
1222
            include_bytes!(
1223
                "../../../test_data/raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01_tile-20_v6.rst"
1224
            ) as &[u8],
1✔
1225
            res[0]
1✔
1226
                .clone()
1✔
1227
                .into_materialized_tile()
1✔
1228
                .grid_array
1✔
1229
                .inner_grid
1✔
1230
                .data
1✔
1231
                .as_slice()
1✔
1232
        );
1233

1234
        Ok(())
2✔
1235
    }
1✔
1236

1237
    #[test]
1238
    fn query_rewrite_4326_3857() {
1✔
1239
        let query = VectorQueryRectangle::new(
1✔
1240
            BoundingBox2D::new_unchecked((-180., -90.).into(), (180., 90.).into()),
1✔
1241
            TimeInterval::default(),
1✔
1242
            ColumnSelection::all(),
1✔
1243
        );
1244

1245
        let expected = BoundingBox2D::new_unchecked(
1✔
1246
            (-20_037_508.342_789_244, -20_048_966.104_014_594).into(),
1✔
1247
            (20_037_508.342_789_244, 20_048_966.104_014_594).into(),
1✔
1248
        );
1249

1250
        let reprojected = reproject_spatial_query(
1✔
1251
            query.spatial_bounds(),
1✔
1252
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857),
1✔
1253
            SpatialReference::epsg_4326(),
1✔
1254
        )
1255
        .unwrap()
1✔
1256
        .unwrap();
1✔
1257

1258
        assert!(approx_eq!(
1✔
1259
            BoundingBox2D,
1260
            expected,
1✔
1261
            reprojected,
1✔
1262
            epsilon = 0.000_001
1263
        ));
1264
    }
1✔
1265

1266
    #[allow(clippy::too_many_lines)]
1267
    #[tokio::test]
1268
    async fn raster_ndvi_3857_to_4326() -> Result<()> {
1✔
1269
        let tile_size_in_pixels = [200, 200].into();
1✔
1270
        let data_geo_transform = GeoTransform::new(
1✔
1271
            Coordinate2D::new(-20_037_508.342_789_244, 19_971_868.880_408_562),
1✔
1272
            14_052.950_258_048_738,
1273
            -14_057.881_117_788_405,
1274
        );
1275
        let data_bounds = GridBoundingBox2D::new([0, 0], [2840, 2850]).unwrap();
1✔
1276
        let result_descriptor = RasterResultDescriptor {
1✔
1277
            data_type: RasterDataType::U8,
1✔
1278
            spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857).into(),
1✔
1279
            time: crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
1280
                Some(TimeInterval::new_unchecked(
1✔
1281
                    TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1282
                    TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1283
                )),
1✔
1284
                TimeStep::months(1),
1✔
1285
            ),
1✔
1286
            spatial_grid: SpatialGridDescriptor::source_from_parts(data_geo_transform, data_bounds),
1✔
1287
            bands: RasterBandDescriptors::new_single_band(),
1✔
1288
        };
1✔
1289

1290
        let m = GdalMetaDataRegular {
1✔
1291
            data_time: TimeInterval::new_unchecked(
1✔
1292
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1293
                TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1294
            ),
1✔
1295
            step: TimeStep {
1✔
1296
                granularity: TimeGranularity::Months,
1✔
1297
                step: 1,
1✔
1298
            },
1✔
1299
            time_placeholders: hashmap! {
1✔
1300
                "%_START_TIME_%".to_string() => GdalSourceTimePlaceholder {
1✔
1301
                    format: DateTimeParseFormat::custom("%Y-%m-%d".to_string()),
1✔
1302
                    reference: TimeReference::Start,
1✔
1303
                },
1✔
1304
            },
1✔
1305
            params: GdalDatasetParameters {
1✔
1306
                file_path: test_data!(
1✔
1307
                    "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_%_START_TIME_%.TIFF"
1✔
1308
                )
1✔
1309
                .into(),
1✔
1310
                rasterband_channel: 1,
1✔
1311
                geo_transform: GdalDatasetGeoTransform {
1✔
1312
                    origin_coordinate: data_geo_transform.origin_coordinate,
1✔
1313
                    x_pixel_size: data_geo_transform.x_pixel_size(),
1✔
1314
                    y_pixel_size: data_geo_transform.y_pixel_size(),
1✔
1315
                },
1✔
1316
                width: data_bounds.axis_size_x(),
1✔
1317
                height: data_bounds.axis_size_y(),
1✔
1318
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1319
                no_data_value: Some(0.),
1✔
1320
                properties_mapping: None,
1✔
1321
                gdal_open_options: None,
1✔
1322
                gdal_config_options: None,
1✔
1323
                allow_alphaband_as_mask: true,
1✔
1324
                retry: None,
1✔
1325
            },
1✔
1326
            result_descriptor: result_descriptor.clone(),
1✔
1327
            cache_ttl: CacheTtlSeconds::default(),
1✔
1328
        };
1✔
1329

1330
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1331
            tile_size_in_pixels,
1✔
1332
        ));
1333

1334
        let id: DataId = DatasetId::new().into();
1✔
1335
        let name = NamedData::with_system_name("ndvi");
1✔
1336
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1337

1338
        let time_interval = TimeInterval::new_unchecked(1_396_310_400_000, 1_396_310_400_000); // 2014-04-01
1✔
1339

1340
        let gdal_op = GdalSource {
1✔
1341
            params: GdalSourceParameters::new(name),
1✔
1342
        }
1✔
1343
        .boxed();
1✔
1344

1345
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1346
            params: ReprojectionParams {
1✔
1347
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1348
                derive_out_spec: DeriveOutRasterSpecsSource::DataBounds,
1✔
1349
            },
1✔
1350
            sources: SingleRasterOrVectorSource {
1✔
1351
                source: gdal_op.into(),
1✔
1352
            },
1✔
1353
        })
1✔
1354
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1355
        .await?;
1✔
1356

1357
        let qp = initialized_operator
1✔
1358
            .query_processor()
1✔
1359
            .unwrap()
1✔
1360
            .get_u8()
1✔
1361
            .unwrap();
1✔
1362

1363
        let qr = qp.result_descriptor();
1✔
1364
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1365

1366
        let qs = qp
1✔
1367
            .raster_query(
1✔
1368
                RasterQueryRectangle::new(
1✔
1369
                    qr.spatial_grid_descriptor()
1✔
1370
                        .tiling_grid_definition(query_ctx.tiling_specification())
1✔
1371
                        .tiling_grid_bounds(),
1✔
1372
                    time_interval,
1✔
1373
                    BandSelection::first(),
1✔
1374
                ),
1✔
1375
                &query_ctx,
1✔
1376
            )
1✔
1377
            .await
1✔
1378
            .unwrap();
1✔
1379

1380
        let tiles = qs
1✔
1381
            .map(Result::unwrap)
1✔
1382
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1383
            .await;
1✔
1384

1385
        // the test should generate 18x10 tiles. However, since the real procucrd pixel size is < 0.1 we will get 20 tiles on the x-axis
1386
        assert_eq!(tiles.len(), /*18*/ 20 * 10);
1✔
1387

1388
        // none of the tiles should be empty
1389
        assert!(tiles.iter().all(|t| !t.is_empty()));
200✔
1390
        Ok(())
2✔
1391
    }
1✔
1392

1393
    #[tokio::test]
1394
    async fn query_outside_projection_area_of_use_produces_empty_tiles() {
1✔
1395
        let tile_size_in_pixels = [600, 600].into();
1✔
1396
        let result_descriptor = RasterResultDescriptor {
1✔
1397
            data_type: RasterDataType::U8,
1✔
1398
            spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636).into(),
1✔
1399
            time: crate::engine::TimeDescriptor::new_irregular(None),
1✔
1400
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1401
                GeoTransform::new(
1✔
1402
                    Coordinate2D::new(166_021.44, 9_329_005.188),
1✔
1403
                    534_994.66 - 166_021.444,
1✔
1404
                    -9_329_005.18,
1✔
1405
                ),
1✔
1406
                GridBoundingBox2D::new_min_max(0, 100, 0, 100).unwrap(),
1✔
1407
            ),
1✔
1408
            bands: RasterBandDescriptors::new_single_band(),
1✔
1409
        };
1✔
1410

1411
        let m = GdalMetaDataStatic {
1✔
1412
            time: Some(TimeInterval::default()),
1✔
1413
            params: GdalDatasetParameters {
1✔
1414
                file_path: PathBuf::new(),
1✔
1415
                rasterband_channel: 1,
1✔
1416
                geo_transform: GdalDatasetGeoTransform {
1✔
1417
                    origin_coordinate: (166_021.44, 9_329_005.188).into(),
1✔
1418
                    x_pixel_size: (534_994.66 - 166_021.444) / 100.,
1✔
1419
                    y_pixel_size: -9_329_005.18 / 100.,
1✔
1420
                },
1✔
1421
                width: 100,
1✔
1422
                height: 100,
1✔
1423
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1424
                no_data_value: Some(0.),
1✔
1425
                properties_mapping: None,
1✔
1426
                gdal_open_options: None,
1✔
1427
                gdal_config_options: None,
1✔
1428
                allow_alphaband_as_mask: true,
1✔
1429
                retry: None,
1✔
1430
            },
1✔
1431
            result_descriptor: result_descriptor.clone(),
1✔
1432
            cache_ttl: CacheTtlSeconds::default(),
1✔
1433
        };
1✔
1434

1435
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1436
            tile_size_in_pixels,
1✔
1437
        ));
1438
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1439

1440
        let id: DataId = DatasetId::new().into();
1✔
1441
        let name = NamedData::with_system_name("ndvi");
1✔
1442
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1443

1444
        let time_interval = TimeInterval::new_instant(1_388_534_400_000).unwrap(); // 2014-01-01
1✔
1445

1446
        let gdal_op = GdalSource {
1✔
1447
            params: GdalSourceParameters::new(name),
1✔
1448
        }
1✔
1449
        .boxed();
1✔
1450

1451
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1452
            params: ReprojectionParams {
1✔
1453
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1454
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1455
            },
1✔
1456
            sources: SingleRasterOrVectorSource {
1✔
1457
                source: gdal_op.into(),
1✔
1458
            },
1✔
1459
        })
1✔
1460
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1461
        .await
1✔
1462
        .unwrap();
1✔
1463

1464
        let qp = initialized_operator
1✔
1465
            .query_processor()
1✔
1466
            .unwrap()
1✔
1467
            .get_u8()
1✔
1468
            .unwrap();
1✔
1469

1470
        let result = qp
1✔
1471
            .raster_query(
1✔
1472
                RasterQueryRectangle::new(
1✔
1473
                    GridBoundingBox2D::new_min_max(500, 1000, 500, 1000).unwrap(),
1✔
1474
                    time_interval,
1✔
1475
                    BandSelection::first(),
1✔
1476
                ),
1✔
1477
                &query_ctx,
1✔
1478
            )
1✔
1479
            .await
1✔
1480
            .unwrap()
1✔
1481
            .map(Result::unwrap)
1✔
1482
            .collect::<Vec<_>>()
1✔
1483
            .await;
1✔
1484

1485
        assert_eq!(result.len(), 4);
1✔
1486

1487
        for r in result {
5✔
1488
            assert!(r.is_empty());
4✔
1489
        }
1✔
1490
    }
1✔
1491

1492
    #[tokio::test]
1493
    async fn points_from_wgs84_to_utm36n() {
1✔
1494
        let exe_ctx = MockExecutionContext::test_default();
1✔
1495
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1496

1497
        let point_source = MockFeatureCollectionSource::single(
1✔
1498
            MultiPointCollection::from_data(
1✔
1499
                MultiPoint::many(vec![
1✔
1500
                    vec![(30.0, 0.0)], // lower left of utm36n area of use
1✔
1501
                    vec![(36.0, 84.0)],
1✔
1502
                    vec![(33.0, 42.0)], // upper right of utm36n area of use
1✔
1503
                ])
1504
                .unwrap(),
1✔
1505
                vec![TimeInterval::default(); 3],
1✔
1506
                HashMap::default(),
1✔
1507
                CacheHint::default(),
1✔
1508
            )
1509
            .unwrap(),
1✔
1510
        )
1511
        .boxed();
1✔
1512

1513
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1514
            params: ReprojectionParams {
1✔
1515
                target_spatial_reference: SpatialReference::new(
1✔
1516
                    SpatialReferenceAuthority::Epsg,
1✔
1517
                    32636, // utm36n
1✔
1518
                ),
1✔
1519
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1520
            },
1✔
1521
            sources: SingleRasterOrVectorSource {
1✔
1522
                source: point_source.into(),
1✔
1523
            },
1✔
1524
        })
1✔
1525
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1526
        .await
1✔
1527
        .unwrap();
1✔
1528

1529
        let qp = initialized_operator
1✔
1530
            .query_processor()
1✔
1531
            .unwrap()
1✔
1532
            .multi_point()
1✔
1533
            .unwrap();
1✔
1534

1535
        let spatial_bounds = BoundingBox2D::new(
1✔
1536
            (166_021.44, 0.00).into(), // lower left of projected utm36n area of use
1✔
1537
            (534_994.666_6, 9_329_005.18).into(), // upper right of projected utm36n area of use
1✔
1538
        )
1539
        .unwrap();
1✔
1540

1541
        let qs = qp
1✔
1542
            .vector_query(
1✔
1543
                VectorQueryRectangle::new(
1✔
1544
                    spatial_bounds,
1✔
1545
                    TimeInterval::default(),
1✔
1546
                    ColumnSelection::all(),
1✔
1547
                ),
1✔
1548
                &query_ctx,
1✔
1549
            )
1✔
1550
            .await
1✔
1551
            .unwrap();
1✔
1552

1553
        let points = qs.map(Result::unwrap).collect::<Vec<_>>().await;
1✔
1554

1555
        assert_eq!(points.len(), 1);
1✔
1556

1557
        let points = &points[0];
1✔
1558

1559
        assert_eq!(
1✔
1560
            points.coordinates(),
1✔
1561
            &[
1✔
1562
                (166_021.443_080_538_42, 0.0).into(),
1✔
1563
                (534_994.655_061_136_1, 9_329_005.182_447_437).into(),
1✔
1564
                (499_999.999_999_999_5, 4_649_776.224_819_178).into()
1✔
1565
            ]
1✔
1566
        );
1✔
1567
    }
1✔
1568

1569
    #[tokio::test]
1570
    async fn points_from_utm36n_to_wgs84() {
1✔
1571
        let exe_ctx = MockExecutionContext::test_default();
1✔
1572
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1573

1574
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1575
            vec![
1✔
1576
                MultiPointCollection::from_data(
1✔
1577
                    MultiPoint::many(vec![
1✔
1578
                        vec![(166_021.443_080_538_42, 0.0)],
1✔
1579
                        vec![(534_994.655_061_136_1, 9_329_005.182_447_437)],
1✔
1580
                        vec![(499_999.999_999_999_5, 4_649_776.224_819_178)],
1✔
1581
                    ])
1582
                    .unwrap(),
1✔
1583
                    vec![TimeInterval::default(); 3],
1✔
1584
                    HashMap::default(),
1✔
1585
                    CacheHint::default(),
1✔
1586
                )
1587
                .unwrap(),
1✔
1588
            ],
1589
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1590
        )
1591
        .boxed();
1✔
1592

1593
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1594
            params: ReprojectionParams {
1✔
1595
                target_spatial_reference: SpatialReference::new(
1✔
1596
                    SpatialReferenceAuthority::Epsg,
1✔
1597
                    4326, // utm36n
1✔
1598
                ),
1✔
1599
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1600
            },
1✔
1601
            sources: SingleRasterOrVectorSource {
1✔
1602
                source: point_source.into(),
1✔
1603
            },
1✔
1604
        })
1✔
1605
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1606
        .await
1✔
1607
        .unwrap();
1✔
1608

1609
        let qp = initialized_operator
1✔
1610
            .query_processor()
1✔
1611
            .unwrap()
1✔
1612
            .multi_point()
1✔
1613
            .unwrap();
1✔
1614

1615
        let spatial_bounds = BoundingBox2D::new(
1✔
1616
            (30.0, 0.0).into(),  // lower left of utm36n area of use
1✔
1617
            (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1618
        )
1619
        .unwrap();
1✔
1620

1621
        let qs = qp
1✔
1622
            .vector_query(
1✔
1623
                VectorQueryRectangle::new(
1✔
1624
                    spatial_bounds,
1✔
1625
                    TimeInterval::default(),
1✔
1626
                    ColumnSelection::all(),
1✔
1627
                ),
1✔
1628
                &query_ctx,
1✔
1629
            )
1✔
1630
            .await
1✔
1631
            .unwrap();
1✔
1632

1633
        let points = qs.map(Result::unwrap).collect::<Vec<_>>().await;
1✔
1634

1635
        assert_eq!(points.len(), 1);
1✔
1636

1637
        let points = &points[0];
1✔
1638

1639
        assert!(approx_eq!(
1✔
1640
            &[Coordinate2D],
1✔
1641
            points.coordinates(),
1✔
1642
            &[
1✔
1643
                (30.0, 0.0).into(), // lower left of utm36n area of use
1✔
1644
                (36.0, 84.0).into(),
1✔
1645
                (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1646
            ]
1✔
1647
        ));
1✔
1648
    }
1✔
1649

1650
    #[tokio::test]
1651
    async fn points_from_utm36n_to_wgs84_out_of_area() {
1✔
1652
        // This test checks that points that are outside the area of use of the target spatial reference are not projected and an empty collection is returned
1653

1654
        let exe_ctx = MockExecutionContext::test_default();
1✔
1655
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1656

1657
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1658
            vec![
1✔
1659
                MultiPointCollection::from_data(
1✔
1660
                    MultiPoint::many(vec![
1✔
1661
                        vec![(758_565., 4_928_353.)], // (12.25, 44,46)
1✔
1662
                    ])
1663
                    .unwrap(),
1✔
1664
                    vec![TimeInterval::default(); 1],
1✔
1665
                    HashMap::default(),
1✔
1666
                    CacheHint::default(),
1✔
1667
                )
1668
                .unwrap(),
1✔
1669
            ],
1670
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1671
        )
1672
        .boxed();
1✔
1673

1674
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1675
            params: ReprojectionParams {
1✔
1676
                target_spatial_reference: SpatialReference::new(
1✔
1677
                    SpatialReferenceAuthority::Epsg,
1✔
1678
                    4326, // utm36n
1✔
1679
                ),
1✔
1680
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1681
            },
1✔
1682
            sources: SingleRasterOrVectorSource {
1✔
1683
                source: point_source.into(),
1✔
1684
            },
1✔
1685
        })
1✔
1686
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1687
        .await
1✔
1688
        .unwrap();
1✔
1689

1690
        let qp = initialized_operator
1✔
1691
            .query_processor()
1✔
1692
            .unwrap()
1✔
1693
            .multi_point()
1✔
1694
            .unwrap();
1✔
1695

1696
        let spatial_bounds = BoundingBox2D::new(
1✔
1697
            (10.0, 0.0).into(),  // -20 x values left of lower left of utm36n area of use
1✔
1698
            (13.0, 42.0).into(), // -20 x values left of upper right of utm36n area of use
1✔
1699
        )
1700
        .unwrap();
1✔
1701

1702
        let qs = qp
1✔
1703
            .vector_query(
1✔
1704
                VectorQueryRectangle::new(
1✔
1705
                    spatial_bounds,
1✔
1706
                    TimeInterval::default(),
1✔
1707
                    ColumnSelection::all(),
1✔
1708
                ),
1✔
1709
                &query_ctx,
1✔
1710
            )
1✔
1711
            .await
1✔
1712
            .unwrap();
1✔
1713

1714
        let points = qs.map(Result::unwrap).collect::<Vec<_>>().await;
1✔
1715

1716
        assert_eq!(points.len(), 1);
1✔
1717

1718
        let points = &points[0];
1✔
1719

1720
        assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1✔
1721
        assert!(points.coordinates().is_empty());
1✔
1722
    }
1✔
1723

1724
    /* TODO resolve the problem with empty intersections
1725
     */
1726
    #[test]
1727
    fn it_derives_raster_result_descriptor() {
1✔
1728
        let in_proj = SpatialReference::epsg_4326();
1✔
1729
        let out_proj = SpatialReference::from_str("EPSG:3857").unwrap();
1✔
1730

1731
        let geo_transform = GeoTransform::new(Coordinate2D::new(0., 0.), 0.1, -0.1);
1✔
1732
        let grid_bounds = GridBoundingBox2D::new_min_max(-850, 849, -1800, 1799).unwrap();
1✔
1733
        let spatial_grid = SpatialGridDefinition::new(geo_transform, grid_bounds);
1✔
1734

1735
        let projector = CoordinateProjector::from_known_srs(in_proj, out_proj).unwrap();
1✔
1736

1737
        let out_spatial_grid = spatial_grid.reproject(&projector).unwrap();
1✔
1738

1739
        assert_eq!(
1✔
1740
            out_spatial_grid.geo_transform.origin_coordinate(),
1✔
1741
            Coordinate2D::new(0., 0.)
1✔
1742
        );
1743

1744
        assert_eq!(
1✔
1745
            out_spatial_grid.geo_transform.spatial_resolution(),
1✔
1746
            SpatialResolution::new_unchecked(14_212.246_793_017_477, 14_212.246_793_017_477)
1✔
1747
        );
1748

1749
        /*
1750
        Projected bounds:
1751
        -20037508.34 -20048966.1
1752
        20037508.34 20048966.1
1753
        */
1754

1755
        assert_eq!(
1✔
1756
            out_spatial_grid.grid_bounds,
1757
            GridBoundingBox2D::new_min_max(-1405, 1405, -1410, 1409).unwrap()
1✔
1758
        );
1759
    }
1✔
1760
}
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