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

geo-engine / geoengine / 19065440894

04 Nov 2025 10:21AM UTC coverage: 88.783%. First build
19065440894

Pull #1083

github

web-flow
Merge 577088df6 into 3d9be4869
Pull Request #1083: feat: new gdal source workflow optimization

5714 of 6540 new or added lines in 71 files covered. (87.37%)

115349 of 129923 relevant lines covered (88.78%)

499710.35 hits per line

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

87.54
/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
        SingleRasterSource, SpatialGridDescriptor, TypedRasterQueryProcessor,
14
        TypedVectorQueryProcessor, VectorOperator, VectorQueryProcessor, VectorResultDescriptor,
15
        WorkflowOperatorPath,
16
    },
17
    error::{self, Error},
18
    optimization::{OptimizableOperator, OptimizationError, ProjectionOptimizationFailed},
19
    processing::{
20
        Downsampling, DownsamplingMethod, DownsamplingParams, DownsamplingResolution,
21
        Interpolation, InterpolationMethod, InterpolationParams, InterpolationResolution,
22
    },
23
    util::{Result, input::RasterOrVectorOperator},
24
};
25
use async_trait::async_trait;
26
use futures::stream::BoxStream;
27
use futures::{StreamExt, stream};
28
use geoengine_datatypes::{
29
    collections::FeatureCollection,
30
    error::BoxedResultExt,
31
    operations::reproject::{
32
        CoordinateProjection, CoordinateProjector, Reproject, ReprojectClipped,
33
        reproject_spatial_query,
34
    },
35
    primitives::{
36
        BandSelection, BoundingBox2D, ColumnSelection, Geometry, RasterQueryRectangle,
37
        SpatialPartition2D, SpatialResolution, VectorQueryRectangle,
38
    },
39
    raster::{GridBoundingBox2D, Pixel, RasterTile2D, TilingSpecification},
40
    spatial_reference::SpatialReference,
41
    util::arrow::ArrowTyped,
42
};
43
use serde::{Deserialize, Serialize};
44
use tracing::trace;
45

46
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
47
#[serde(rename_all = "camelCase")]
48
pub enum DeriveOutRasterSpecsSource {
49
    DataBounds,
50
    ProjectionBounds,
51
}
52
impl Default for DeriveOutRasterSpecsSource {
53
    fn default() -> Self {
3✔
54
        Self::ProjectionBounds
3✔
55
    }
3✔
56
}
57

58
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
59
#[serde(rename_all = "camelCase")]
60
pub struct ReprojectionParams {
61
    pub target_spatial_reference: SpatialReference,
62
    #[serde(default)]
63
    pub derive_out_spec: DeriveOutRasterSpecsSource,
64
}
65

66
pub type Reprojection = Operator<ReprojectionParams, SingleRasterOrVectorSource>;
67

68
impl Reprojection {}
69

70
impl OperatorName for Reprojection {
71
    const TYPE_NAME: &'static str = "Reprojection";
72
}
73

74
pub struct InitializedVectorReprojection {
75
    name: CanonicOperatorName,
76
    path: WorkflowOperatorPath,
77
    result_descriptor: VectorResultDescriptor,
78
    params: ReprojectionParams,
79
    source: Box<dyn InitializedVectorOperator>,
80
    source_srs: SpatialReference,
81
    target_srs: SpatialReference,
82
}
83

84
pub struct InitializedRasterReprojection<O: InitializedRasterOperator> {
85
    name: CanonicOperatorName,
86
    path: WorkflowOperatorPath,
87
    result_descriptor: RasterResultDescriptor,
88
    params: ReprojectionParams,
89
    source: O,
90
    state: TileReprojectionSubqueryGridInfo,
91
    source_srs: SpatialReference,
92
    target_srs: SpatialReference,
93
}
94

95
impl InitializedVectorReprojection {
96
    /// Create a new `InitializedVectorReprojection` instance.
97
    /// The `source` must have the same `SpatialReference` as `source_srs`.
98
    ///
99
    /// # Errors
100
    /// This function errors if the `source`'s `SpatialReference` is `None`.
101
    /// This function errors if the source's bounding box cannot be reprojected to the target's `SpatialReference`.
102
    pub fn try_new_with_input(
8✔
103
        name: CanonicOperatorName,
8✔
104
        path: WorkflowOperatorPath,
8✔
105
        params: ReprojectionParams,
8✔
106
        source_vector_operator: Box<dyn InitializedVectorOperator>,
8✔
107
    ) -> Result<Self> {
8✔
108
        let in_desc: VectorResultDescriptor = source_vector_operator.result_descriptor().clone();
8✔
109

110
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
8✔
111
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
8✔
112

113
        let bbox = if let Some(bbox) = in_desc.bbox {
8✔
114
            let projector =
2✔
115
                CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
2✔
116

117
            bbox.reproject_clipped(&projector)? // TODO: if this is none then we could skip the whole reprojection similar to raster?
2✔
118
        } else {
119
            None
6✔
120
        };
121

122
        let out_desc = VectorResultDescriptor {
8✔
123
            spatial_reference: params.target_spatial_reference.into(),
8✔
124
            data_type: in_desc.data_type,
8✔
125
            columns: in_desc.columns.clone(),
8✔
126
            time: in_desc.time,
8✔
127
            bbox,
8✔
128
        };
8✔
129

130
        Ok(InitializedVectorReprojection {
8✔
131
            name,
8✔
132
            path,
8✔
133
            result_descriptor: out_desc,
8✔
134
            params,
8✔
135
            source: source_vector_operator,
8✔
136
            source_srs: in_srs,
8✔
137
            target_srs: params.target_spatial_reference,
8✔
138
        })
8✔
139
    }
8✔
140
}
141

142
impl<O: InitializedRasterOperator> InitializedRasterReprojection<O> {
143
    pub fn try_new_with_input(
7✔
144
        name: CanonicOperatorName,
7✔
145
        path: WorkflowOperatorPath,
7✔
146
        params: ReprojectionParams,
7✔
147
        source_raster_operator: O,
7✔
148
        tiling_spec: TilingSpecification,
7✔
149
    ) -> Result<Self> {
7✔
150
        let in_desc: RasterResultDescriptor = source_raster_operator.result_descriptor().clone();
7✔
151
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
7✔
152
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
7✔
153

154
        // calculate the intersection of input and output srs in both coordinate systems
155
        let proj_from_to =
7✔
156
            CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
7✔
157

158
        let out_spatial_grid = match params.derive_out_spec {
7✔
159
            DeriveOutRasterSpecsSource::DataBounds => in_desc
1✔
160
                .spatial_grid_descriptor()
1✔
161
                .reproject_clipped(&proj_from_to)?, // TODO: we could skip the intersection (clipped) and try to project larger area
1✔
162
            DeriveOutRasterSpecsSource::ProjectionBounds => {
163
                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?
6✔
164
                let target_proj_total_grid = in_desc
6✔
165
                    .spatial_grid_descriptor()
6✔
166
                    .spatial_bounds_to_compatible_spatial_grid(in_srs_area)
6✔
167
                    .reproject_clipped(&proj_from_to)?;
6✔
168
                // TODO: we could skip the intersection and try to project larger area
169
                let spatial_bounds_proj =
6✔
170
                    in_desc.spatial_bounds().reproject_clipped(&proj_from_to)?;
6✔
171
                target_proj_total_grid.and_then(|x| {
6✔
172
                    spatial_bounds_proj.map(|spb| x.spatial_bounds_to_compatible_spatial_grid(spb))
6✔
173
                })
6✔
174
            }
175
        };
176

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

180
        let out_desc = RasterResultDescriptor {
7✔
181
            spatial_reference: params.target_spatial_reference.into(),
7✔
182
            data_type: in_desc.data_type,
7✔
183
            time: in_desc.time,
7✔
184
            spatial_grid: out_spatial_grid,
7✔
185
            bands: in_desc.bands.clone(),
7✔
186
        };
7✔
187

188
        let state = TileReprojectionSubqueryGridInfo {
7✔
189
            in_spatial_grid: in_desc
7✔
190
                .spatial_grid_descriptor()
7✔
191
                .tiling_grid_definition(tiling_spec)
7✔
192
                .tiling_spatial_grid_definition(),
7✔
193
            out_spatial_grid: out_spatial_grid
7✔
194
                .tiling_grid_definition(tiling_spec)
7✔
195
                .tiling_spatial_grid_definition(),
7✔
196
        };
7✔
197

198
        Ok(InitializedRasterReprojection {
7✔
199
            name,
7✔
200
            path,
7✔
201
            params,
7✔
202
            result_descriptor: out_desc,
7✔
203
            source: source_raster_operator,
7✔
204
            state,
7✔
205
            source_srs: in_srs,
7✔
206
            target_srs: params.target_spatial_reference,
7✔
207
        })
7✔
208
    }
7✔
209
}
210

211
fn compute_output_spatial_grid(
1✔
212
    in_spatial_grid_descriptor: SpatialGridDescriptor,
1✔
213
    in_spatial_bounds: SpatialPartition2D,
1✔
214
    in_srs: SpatialReference,
1✔
215
    params: ReprojectionParams,
1✔
216
) -> Result<SpatialGridDescriptor> {
1✔
217
    let proj_from_to =
1✔
218
        CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
1✔
219
    let out_spatial_grid = match params.derive_out_spec {
1✔
220
        DeriveOutRasterSpecsSource::DataBounds => {
NEW
221
            in_spatial_grid_descriptor.reproject_clipped(&proj_from_to)?
×
222
        }
223
        DeriveOutRasterSpecsSource::ProjectionBounds => {
224
            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?
1✔
225
            let target_proj_total_grid = in_spatial_grid_descriptor
1✔
226
                .spatial_bounds_to_compatible_spatial_grid(in_srs_area)
1✔
227
                .reproject_clipped(&proj_from_to)?;
1✔
228
            // jetzt grid mit origin (tl) auf grid vom dataset. dann umprojeziren. Dann intersection mit boundingbox in dataset
229
            let spatial_bounds_proj = in_spatial_bounds.reproject_clipped(&proj_from_to)?;
1✔
230
            target_proj_total_grid.and_then(|x| {
1✔
231
                spatial_bounds_proj.map(|spb| x.spatial_bounds_to_compatible_spatial_grid(spb))
1✔
232
            })
1✔
233
        }
234
    };
235
    let out_spatial_grid = out_spatial_grid.ok_or(error::Error::ReprojectionFailed)?;
1✔
236
    Ok(out_spatial_grid)
1✔
237
}
1✔
238

239
#[typetag::serde]
×
240
#[async_trait]
241
impl VectorOperator for Reprojection {
242
    async fn _initialize(
243
        self: Box<Self>,
244
        path: WorkflowOperatorPath,
245
        context: &dyn ExecutionContext,
246
    ) -> Result<Box<dyn InitializedVectorOperator>> {
9✔
247
        let name = CanonicOperatorName::from(&self);
248

249
        let vector_source =
250
            self.sources
251
                .vector()
252
                .ok_or_else(|| error::Error::InvalidOperatorType {
253
                    expected: "Vector".to_owned(),
1✔
254
                    found: "Raster".to_owned(),
1✔
255
                })?;
1✔
256

257
        let initialized_source = vector_source
258
            .initialize_sources(path.clone(), context)
259
            .await?;
260

261
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
262
            name,
263
            path,
264
            self.params,
265
            initialized_source.vector,
266
        )?;
267

268
        Ok(initialized_operator.boxed())
269
    }
9✔
270

271
    span_fn!(Reprojection);
272
}
273

274
impl InitializedVectorOperator for InitializedVectorReprojection {
275
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
276
        &self.result_descriptor
×
277
    }
×
278

279
    fn query_processor(&self) -> Result<TypedVectorQueryProcessor> {
6✔
280
        let source_srs = self.source_srs;
6✔
281
        let target_srs = self.target_srs;
6✔
282
        match self.source.query_processor()? {
6✔
283
            TypedVectorQueryProcessor::Data(source) => Ok(TypedVectorQueryProcessor::Data(
×
284
                MapQueryProcessor::new(source, move |query: VectorQueryRectangle| {
×
285
                    reproject_spatial_query(query.spatial_bounds(), source_srs, target_srs, false)
×
286
                        .map(|sqr| {
×
287
                            sqr.map(|x| {
×
288
                                VectorQueryRectangle::new(
×
289
                                    x,
×
290
                                    query.time_interval(),
×
291
                                    *query.attributes(),
×
292
                                )
293
                            })
×
294
                        })
×
295
                        .map_err(From::from)
×
296
                })
×
297
                .boxed(),
×
298
            )),
299
            TypedVectorQueryProcessor::MultiPoint(source) => {
4✔
300
                Ok(TypedVectorQueryProcessor::MultiPoint(
4✔
301
                    VectorReprojectionProcessor::new(
4✔
302
                        source,
4✔
303
                        self.result_descriptor.clone(),
4✔
304
                        source_srs,
4✔
305
                        target_srs,
4✔
306
                    )
4✔
307
                    .boxed(),
4✔
308
                ))
4✔
309
            }
310
            TypedVectorQueryProcessor::MultiLineString(source) => {
1✔
311
                Ok(TypedVectorQueryProcessor::MultiLineString(
1✔
312
                    VectorReprojectionProcessor::new(
1✔
313
                        source,
1✔
314
                        self.result_descriptor.clone(),
1✔
315
                        source_srs,
1✔
316
                        target_srs,
1✔
317
                    )
1✔
318
                    .boxed(),
1✔
319
                ))
1✔
320
            }
321
            TypedVectorQueryProcessor::MultiPolygon(source) => {
1✔
322
                Ok(TypedVectorQueryProcessor::MultiPolygon(
1✔
323
                    VectorReprojectionProcessor::new(
1✔
324
                        source,
1✔
325
                        self.result_descriptor.clone(),
1✔
326
                        source_srs,
1✔
327
                        target_srs,
1✔
328
                    )
1✔
329
                    .boxed(),
1✔
330
                ))
1✔
331
            }
332
        }
333
    }
6✔
334

335
    fn canonic_name(&self) -> CanonicOperatorName {
×
336
        self.name.clone()
×
337
    }
×
338

339
    fn name(&self) -> &'static str {
×
340
        Reprojection::TYPE_NAME
×
341
    }
×
342

343
    fn path(&self) -> WorkflowOperatorPath {
×
344
        self.path.clone()
×
345
    }
×
346

347
    fn optimize(
2✔
348
        &self,
2✔
349
        target_resolution: SpatialResolution,
2✔
350
    ) -> Result<Box<dyn VectorOperator>, OptimizationError> {
2✔
351
        self.source
2✔
352
            .optimize(target_resolution)
2✔
353
            .map(|optimized_source| {
2✔
354
                Box::new(Reprojection {
2✔
355
                    params: self.params,
2✔
356
                    sources: SingleRasterOrVectorSource {
2✔
357
                        source: RasterOrVectorOperator::Vector(optimized_source),
2✔
358
                    },
2✔
359
                }) as Box<dyn VectorOperator>
2✔
360
            })
2✔
361
    }
2✔
362
}
363

364
struct VectorReprojectionProcessor<Q, G>
365
where
366
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
367
{
368
    source: Q,
369
    result_descriptor: VectorResultDescriptor,
370
    from: SpatialReference,
371
    to: SpatialReference,
372
}
373

374
impl<Q, G> VectorReprojectionProcessor<Q, G>
375
where
376
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
377
{
378
    pub fn new(
6✔
379
        source: Q,
6✔
380
        result_descriptor: VectorResultDescriptor,
6✔
381
        from: SpatialReference,
6✔
382
        to: SpatialReference,
6✔
383
    ) -> Self {
6✔
384
        Self {
6✔
385
            source,
6✔
386
            result_descriptor,
6✔
387
            from,
6✔
388
            to,
6✔
389
        }
6✔
390
    }
6✔
391
}
392

393
#[async_trait]
394
impl<Q, G> QueryProcessor for VectorReprojectionProcessor<Q, G>
395
where
396
    Q: QueryProcessor<
397
            Output = FeatureCollection<G>,
398
            SpatialBounds = BoundingBox2D,
399
            Selection = ColumnSelection,
400
            ResultDescription = VectorResultDescriptor,
401
        >,
402
    FeatureCollection<G>: Reproject<CoordinateProjector, Out = FeatureCollection<G>>,
403
    G: Geometry + ArrowTyped,
404
{
405
    type Output = FeatureCollection<G>;
406
    type SpatialBounds = BoundingBox2D;
407
    type Selection = ColumnSelection;
408
    type ResultDescription = VectorResultDescriptor;
409

410
    async fn _query<'a>(
411
        &'a self,
412
        query: VectorQueryRectangle,
413
        ctx: &'a dyn QueryContext,
414
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
6✔
415
        let rewritten_spatial_query =
416
            reproject_spatial_query(query.spatial_bounds(), self.from, self.to, false)?;
417

418
        let rewritten_query = rewritten_spatial_query.map(|rwq| query.select_spatial_bounds(rwq));
6✔
419

420
        if let Some(rewritten_query) = rewritten_query {
421
            Ok(self
422
                .source
423
                .query(rewritten_query, ctx)
424
                .await?
425
                .map(move |collection_result| {
6✔
426
                    collection_result.and_then(|collection| {
6✔
427
                        CoordinateProjector::from_known_srs(self.from, self.to)
6✔
428
                            .and_then(|projector| collection.reproject(projector.as_ref()))
6✔
429
                            .map_err(Into::into)
6✔
430
                    })
6✔
431
                })
6✔
432
                .boxed())
433
        } else {
434
            let res = Ok(FeatureCollection::empty());
435
            Ok(Box::pin(stream::once(async { res })))
×
436
        }
437
    }
6✔
438

439
    fn result_descriptor(&self) -> &VectorResultDescriptor {
12✔
440
        &self.result_descriptor
12✔
441
    }
12✔
442
}
443

444
#[typetag::serde]
×
445
#[async_trait]
446
impl RasterOperator for Reprojection {
447
    async fn _initialize(
448
        self: Box<Self>,
449
        path: WorkflowOperatorPath,
450
        context: &dyn ExecutionContext,
451
    ) -> Result<Box<dyn InitializedRasterOperator>> {
7✔
452
        let name = CanonicOperatorName::from(&self);
453

454
        let raster_source =
455
            self.sources
456
                .raster()
457
                .ok_or_else(|| error::Error::InvalidOperatorType {
458
                    expected: "Raster".to_owned(),
×
459
                    found: "Vector".to_owned(),
×
460
                })?;
×
461

462
        let initialized_source = raster_source
463
            .initialize_sources(path.clone(), context)
464
            .await?;
465

466
        let initialized_operator = InitializedRasterReprojection::try_new_with_input(
467
            name,
468
            path,
469
            self.params,
470
            initialized_source.raster,
471
            context.tiling_specification(),
472
        )?;
473

474
        Ok(initialized_operator.boxed())
475
    }
7✔
476

477
    span_fn!(Reprojection);
478
}
479

480
impl<O: InitializedRasterOperator> InitializedRasterOperator for InitializedRasterReprojection<O> {
481
    fn result_descriptor(&self) -> &RasterResultDescriptor {
4✔
482
        &self.result_descriptor
4✔
483
    }
4✔
484

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

490
        Ok(match self.result_descriptor.data_type {
4✔
491
            geoengine_datatypes::raster::RasterDataType::U8 => {
492
                let qt = q
4✔
493
                    .get_u8()
4✔
494
                    .expect("the result descriptor and query processor type should match");
4✔
495
                TypedRasterQueryProcessor::U8(Box::new(RasterReprojectionProcessor::new(
4✔
496
                    qt,
4✔
497
                    self.result_descriptor.clone(),
4✔
498
                    self.source_srs,
4✔
499
                    self.target_srs,
4✔
500
                    self.state,
4✔
501
                )))
4✔
502
            }
503
            geoengine_datatypes::raster::RasterDataType::U16 => {
504
                let qt = q
×
505
                    .get_u16()
×
506
                    .expect("the result descriptor and query processor type should match");
×
507
                TypedRasterQueryProcessor::U16(Box::new(RasterReprojectionProcessor::new(
×
508
                    qt,
×
509
                    self.result_descriptor.clone(),
×
510
                    self.source_srs,
×
511
                    self.target_srs,
×
512
                    self.state,
×
513
                )))
×
514
            }
515

516
            geoengine_datatypes::raster::RasterDataType::U32 => {
517
                let qt = q
×
518
                    .get_u32()
×
519
                    .expect("the result descriptor and query processor type should match");
×
520
                TypedRasterQueryProcessor::U32(Box::new(RasterReprojectionProcessor::new(
×
521
                    qt,
×
522
                    self.result_descriptor.clone(),
×
523
                    self.source_srs,
×
524
                    self.target_srs,
×
525
                    self.state,
×
526
                )))
×
527
            }
528
            geoengine_datatypes::raster::RasterDataType::U64 => {
529
                let qt = q
×
530
                    .get_u64()
×
531
                    .expect("the result descriptor and query processor type should match");
×
532
                TypedRasterQueryProcessor::U64(Box::new(RasterReprojectionProcessor::new(
×
533
                    qt,
×
534
                    self.result_descriptor.clone(),
×
535
                    self.source_srs,
×
536
                    self.target_srs,
×
537
                    self.state,
×
538
                )))
×
539
            }
540
            geoengine_datatypes::raster::RasterDataType::I8 => {
541
                let qt = q
×
542
                    .get_i8()
×
543
                    .expect("the result descriptor and query processor type should match");
×
544
                TypedRasterQueryProcessor::I8(Box::new(RasterReprojectionProcessor::new(
×
545
                    qt,
×
546
                    self.result_descriptor.clone(),
×
547
                    self.source_srs,
×
548
                    self.target_srs,
×
549
                    self.state,
×
550
                )))
×
551
            }
552
            geoengine_datatypes::raster::RasterDataType::I16 => {
553
                let qt = q
×
554
                    .get_i16()
×
555
                    .expect("the result descriptor and query processor type should match");
×
556
                TypedRasterQueryProcessor::I16(Box::new(RasterReprojectionProcessor::new(
×
557
                    qt,
×
558
                    self.result_descriptor.clone(),
×
559
                    self.source_srs,
×
560
                    self.target_srs,
×
561
                    self.state,
×
562
                )))
×
563
            }
564
            geoengine_datatypes::raster::RasterDataType::I32 => {
565
                let qt = q
×
566
                    .get_i32()
×
567
                    .expect("the result descriptor and query processor type should match");
×
568
                TypedRasterQueryProcessor::I32(Box::new(RasterReprojectionProcessor::new(
×
569
                    qt,
×
570
                    self.result_descriptor.clone(),
×
571
                    self.source_srs,
×
572
                    self.target_srs,
×
573
                    self.state,
×
574
                )))
×
575
            }
576
            geoengine_datatypes::raster::RasterDataType::I64 => {
577
                let qt = q
×
578
                    .get_i64()
×
579
                    .expect("the result descriptor and query processor type should match");
×
580
                TypedRasterQueryProcessor::I64(Box::new(RasterReprojectionProcessor::new(
×
581
                    qt,
×
582
                    self.result_descriptor.clone(),
×
583
                    self.source_srs,
×
584
                    self.target_srs,
×
585
                    self.state,
×
586
                )))
×
587
            }
588
            geoengine_datatypes::raster::RasterDataType::F32 => {
589
                let qt = q
×
590
                    .get_f32()
×
591
                    .expect("the result descriptor and query processor type should match");
×
592
                TypedRasterQueryProcessor::F32(Box::new(RasterReprojectionProcessor::new(
×
593
                    qt,
×
594
                    self.result_descriptor.clone(),
×
595
                    self.source_srs,
×
596
                    self.target_srs,
×
597
                    self.state,
×
598
                )))
×
599
            }
600
            geoengine_datatypes::raster::RasterDataType::F64 => {
601
                let qt = q
×
602
                    .get_f64()
×
603
                    .expect("the result descriptor and query processor type should match");
×
604
                TypedRasterQueryProcessor::F64(Box::new(RasterReprojectionProcessor::new(
×
605
                    qt,
×
606
                    self.result_descriptor.clone(),
×
607
                    self.source_srs,
×
608
                    self.target_srs,
×
609
                    self.state,
×
610
                )))
×
611
            }
612
        })
613
    }
4✔
614

615
    fn canonic_name(&self) -> CanonicOperatorName {
×
616
        self.name.clone()
×
617
    }
×
618

619
    fn name(&self) -> &'static str {
×
620
        Reprojection::TYPE_NAME
×
621
    }
×
622

623
    fn path(&self) -> WorkflowOperatorPath {
×
624
        self.path.clone()
×
625
    }
×
626

627
    fn optimize(
1✔
628
        &self,
1✔
629
        target_resolution: SpatialResolution,
1✔
630
    ) -> Result<Box<dyn RasterOperator>, OptimizationError> {
1✔
631
        // adjust input resolution relative to change in target resolution
632
        // idea: optimization of reprojection snaps from one overview level to another, so do the same in the source
633
        // if the result does not match, we need to interpolate or resample the result to match the target resolution
634

635
        // TODO: validate the quality of this approach, especially if the output resolution is actually the target resolution
636

637
        self.ensure_resolution_is_compatible_for_optimization(target_resolution)?;
1✔
638

639
        let current_resolution = self.result_descriptor.spatial_grid.spatial_resolution();
1✔
640

641
        // TODO: use `find_next_best_resolution_overview_level` instead?
642
        let output_overview_level_factor =
1✔
643
            (target_resolution.x / current_resolution.x).round() as u32;
1✔
644
        // must be the same in x and y
645
        debug_assert_eq!(
1✔
646
            output_overview_level_factor,
647
            (target_resolution.y / current_resolution.y).round() as u32
1✔
648
        );
649
        // must be a power of 2
650
        debug_assert!(output_overview_level_factor.is_power_of_two());
1✔
651

652
        let input_descriptor = self.source.result_descriptor();
1✔
653

654
        let input_resolution = input_descriptor.spatial_grid.spatial_resolution();
1✔
655
        let scaled_input_resolution = input_resolution * f64::from(output_overview_level_factor);
1✔
656

657
        let source_optimized = self.source.optimize(scaled_input_resolution)?;
1✔
658

659
        // given the input resolution, derive_out_spec, input and output spatial reference, calculate the output spatial grid
660
        // TODO: instead of repeating the logic here, we could attach the result descriptor to the result of the `optimize` method
661
        let optimized_spatial_grid_descriptor = input_descriptor
1✔
662
            .spatial_grid_descriptor()
1✔
663
            .with_changed_resolution(scaled_input_resolution);
1✔
664

665
        // TODO: check if these are the correct bounds
666
        let optimized_spatial_bounds = optimized_spatial_grid_descriptor.spatial_partition();
1✔
667

668
        let output_spatial_grid = compute_output_spatial_grid(
1✔
669
            optimized_spatial_grid_descriptor,
1✔
670
            optimized_spatial_bounds,
1✔
671
            self.source_srs,
1✔
672
            self.params,
1✔
673
        )
674
        .boxed_context(ProjectionOptimizationFailed)?;
1✔
675

676
        let output_spatial_resolution = output_spatial_grid.spatial_resolution();
1✔
677
        trace!(
1✔
NEW
678
            "output_spatial_resolution: {output_spatial_resolution:?}, target_resolution: {target_resolution:?}"
×
679
        );
680

681
        let optimized_reprojection = Box::new(Reprojection {
1✔
682
            params: ReprojectionParams {
1✔
683
                target_spatial_reference: self.target_srs,
1✔
684
                derive_out_spec: self.params.derive_out_spec,
1✔
685
            },
1✔
686
            sources: SingleRasterOrVectorSource {
1✔
687
                source: RasterOrVectorOperator::Raster(source_optimized),
1✔
688
            },
1✔
689
        });
1✔
690

691
        if output_spatial_resolution == target_resolution {
1✔
NEW
692
            return Ok(optimized_reprojection);
×
693
        }
1✔
694

695
        // depending on the reprojection, the reprojection of the optimized input might not perfectly match the target resolution, so we may need to fix it
696

697
        if output_spatial_resolution > target_resolution {
1✔
698
            // reprojected raster is too coarse, we need to interpolate
699
            // TODO: in this case we could (should?) also decrease the `scaled_input_resolution` to the previous overview level to get a finer result
700
            return Ok(Interpolation {
1✔
701
                params: InterpolationParams {
1✔
702
                    interpolation: InterpolationMethod::NearestNeighbor,
1✔
703
                    output_resolution: InterpolationResolution::Resolution(target_resolution),
1✔
704
                    output_origin_reference: None,
1✔
705
                },
1✔
706
                sources: SingleRasterSource {
1✔
707
                    raster: optimized_reprojection,
1✔
708
                },
1✔
709
            }
1✔
710
            .boxed());
1✔
NEW
711
        }
×
712

713
        // reprojected raster is too fine, we need to downsample
NEW
714
        Ok(Downsampling {
×
NEW
715
            params: DownsamplingParams {
×
NEW
716
                sampling_method: DownsamplingMethod::NearestNeighbor,
×
NEW
717
                output_resolution: DownsamplingResolution::Resolution(target_resolution),
×
NEW
718
                output_origin_reference: None,
×
NEW
719
            },
×
NEW
720
            sources: SingleRasterSource {
×
NEW
721
                raster: optimized_reprojection,
×
NEW
722
            },
×
NEW
723
        }
×
NEW
724
        .boxed())
×
725
    }
1✔
726
}
727

728
pub struct RasterReprojectionProcessor<Q, P>
729
where
730
    Q: RasterQueryProcessor<RasterType = P>,
731
{
732
    source: Q,
733
    result_descriptor: RasterResultDescriptor,
734
    from: SpatialReference,
735
    to: SpatialReference,
736
    state: TileReprojectionSubqueryGridInfo,
737
    _phantom_data: PhantomData<P>,
738
}
739

740
impl<Q, P> RasterReprojectionProcessor<Q, P>
741
where
742
    Q: RasterQueryProcessor<RasterType = P>,
743
    P: Pixel,
744
{
745
    pub fn new(
4✔
746
        source: Q,
4✔
747
        result_descriptor: RasterResultDescriptor,
4✔
748
        from: SpatialReference,
4✔
749
        to: SpatialReference,
4✔
750
        state: TileReprojectionSubqueryGridInfo,
4✔
751
    ) -> Self {
4✔
752
        Self {
4✔
753
            source,
4✔
754
            result_descriptor,
4✔
755
            from,
4✔
756
            to,
4✔
757
            state,
4✔
758
            _phantom_data: PhantomData,
4✔
759
        }
4✔
760
    }
4✔
761
}
762

763
#[async_trait]
764
impl<Q, P> QueryProcessor for RasterReprojectionProcessor<Q, P>
765
where
766
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
767
    P: Pixel,
768
{
769
    type Output = RasterTile2D<P>;
770
    type SpatialBounds = GridBoundingBox2D;
771
    type Selection = BandSelection;
772
    type ResultDescription = RasterResultDescriptor;
773

774
    async fn _query<'a>(
775
        &'a self,
776
        query: RasterQueryRectangle,
777
        ctx: &'a dyn QueryContext,
778
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
4✔
779
        let state = self.state;
780

781
        // setup the subquery
782
        let sub_query_spec = TileReprojectionSubQuery {
783
            in_srs: self.from,
784
            out_srs: self.to,
785
            fold_fn: fold_by_coordinate_lookup_future,
786
            state,
787
            _phantom_data: PhantomData,
788
        };
789

790
        let tiling_strat = self
791
            .result_descriptor
792
            .spatial_grid_descriptor()
793
            .tiling_grid_definition(ctx.tiling_specification())
794
            .generate_data_tiling_strategy();
795

796
        let time_stream = self.time_query(query.time_interval(), ctx).await?;
797

798
        // return the adapter which will reproject the tiles and uses the fill adapter to inject missing tiles
799
        Ok(RasterSubQueryAdapter::<'a, P, _, _, _>::new(
800
            &self.source,
801
            query,
802
            tiling_strat,
803
            ctx,
804
            sub_query_spec,
805
            time_stream,
806
        )
807
        .box_pin())
808
    }
4✔
809

810
    fn result_descriptor(&self) -> &RasterResultDescriptor {
10✔
811
        &self.result_descriptor
10✔
812
    }
10✔
813
}
814

815
#[async_trait]
816
impl<Q, P> RasterQueryProcessor for RasterReprojectionProcessor<Q, P>
817
where
818
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
819
    P: Pixel,
820
{
821
    type RasterType = P;
822

823
    async fn time_query<'a>(
824
        &'a self,
825
        query: geoengine_datatypes::primitives::TimeInterval,
826
        ctx: &'a dyn QueryContext,
827
    ) -> Result<BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>> {
4✔
828
        self.source.time_query(query, ctx).await
829
    }
4✔
830
}
831

832
#[cfg(test)]
833
mod tests {
834
    use super::*;
835
    use crate::engine::{MockExecutionContext, RasterBandDescriptors, SpatialGridDescriptor};
836
    use crate::mock::MockFeatureCollectionSource;
837
    use crate::mock::{MockRasterSource, MockRasterSourceParams};
838
    use crate::source::{
839
        FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters, GdalMetaDataRegular,
840
        GdalMetaDataStatic, GdalSourceTimePlaceholder, TimeReference,
841
    };
842
    use crate::util::gdal::add_ndvi_dataset;
843
    use crate::{
844
        engine::{ChunkByteSize, VectorOperator},
845
        source::{GdalSource, GdalSourceParameters},
846
        test_data,
847
    };
848
    use float_cmp::{approx_eq, assert_approx_eq};
849
    use futures::StreamExt;
850
    use geoengine_datatypes::collections::IntoGeometryIterator;
851
    use geoengine_datatypes::dataset::{DataId, DatasetId, NamedData};
852
    use geoengine_datatypes::hashmap;
853
    use geoengine_datatypes::primitives::{
854
        CacheHint, CacheTtlSeconds, DateTimeParseFormat, SpatialResolution, TimeGranularity,
855
        TimeInstance,
856
    };
857
    use geoengine_datatypes::primitives::{Coordinate2D, TimeStep};
858
    use geoengine_datatypes::raster::{
859
        GeoTransform, GridBoundingBox2D, GridShape2D, GridSize, SpatialGridDefinition,
860
        TilesEqualIgnoringCacheHint,
861
    };
862
    use geoengine_datatypes::{
863
        collections::{
864
            GeometryCollection, MultiLineStringCollection, MultiPointCollection,
865
            MultiPolygonCollection,
866
        },
867
        primitives::{BoundingBox2D, MultiLineString, MultiPoint, MultiPolygon, TimeInterval},
868
        raster::{Grid, RasterDataType, RasterTile2D},
869
        spatial_reference::SpatialReferenceAuthority,
870
        util::{
871
            Identifier,
872
            test::TestDefault,
873
            well_known_data::{
874
                COLOGNE_EPSG_900_913, COLOGNE_EPSG_4326, HAMBURG_EPSG_900_913, HAMBURG_EPSG_4326,
875
                MARBURG_EPSG_900_913, MARBURG_EPSG_4326,
876
            },
877
        },
878
    };
879
    use std::collections::HashMap;
880
    use std::path::PathBuf;
881
    use std::str::FromStr;
882

883
    #[tokio::test]
884
    async fn multi_point() -> Result<()> {
1✔
885
        let points = MultiPointCollection::from_data(
1✔
886
            MultiPoint::many(vec![
1✔
887
                MARBURG_EPSG_4326,
888
                COLOGNE_EPSG_4326,
889
                HAMBURG_EPSG_4326,
890
            ])
891
            .unwrap(),
1✔
892
            vec![TimeInterval::new_unchecked(0, 1); 3],
1✔
893
            Default::default(),
1✔
894
            CacheHint::default(),
1✔
895
        )?;
×
896

897
        let expected = MultiPoint::many(vec![
1✔
898
            MARBURG_EPSG_900_913,
899
            COLOGNE_EPSG_900_913,
900
            HAMBURG_EPSG_900_913,
901
        ])
902
        .unwrap();
1✔
903

904
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
905

906
        let target_spatial_reference =
1✔
907
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
908

909
        let exe_ctx = MockExecutionContext::test_default();
1✔
910

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

923
        let query_processor = initialized_operator.query_processor()?;
1✔
924

925
        let query_processor = query_processor.multi_point().unwrap();
1✔
926

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

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

940
        let result = query
1✔
941
            .map(Result::unwrap)
1✔
942
            .collect::<Vec<MultiPointCollection>>()
1✔
943
            .await;
1✔
944

945
        assert_eq!(result.len(), 1);
1✔
946

947
        result[0]
1✔
948
            .geometries()
1✔
949
            .zip(expected.iter())
1✔
950
            .for_each(|(a, e)| {
3✔
951
                assert!(approx_eq!(&MultiPoint, &a.into(), e, epsilon = 0.00001));
3✔
952
            });
3✔
953

954
        Ok(())
2✔
955
    }
1✔
956

957
    #[tokio::test]
958
    async fn multi_lines() -> Result<()> {
1✔
959
        let lines = MultiLineStringCollection::from_data(
1✔
960
            vec![
1✔
961
                MultiLineString::new(vec![vec![
1✔
962
                    MARBURG_EPSG_4326,
963
                    COLOGNE_EPSG_4326,
964
                    HAMBURG_EPSG_4326,
965
                ]])
966
                .unwrap(),
1✔
967
            ],
968
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
969
            Default::default(),
1✔
970
            CacheHint::default(),
1✔
971
        )?;
×
972

973
        let expected = [MultiLineString::new(vec![vec![
1✔
974
            MARBURG_EPSG_900_913,
1✔
975
            COLOGNE_EPSG_900_913,
1✔
976
            HAMBURG_EPSG_900_913,
1✔
977
        ]])
1✔
978
        .unwrap()];
1✔
979

980
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
981

982
        let target_spatial_reference =
1✔
983
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
984

985
        let exe_ctx = MockExecutionContext::test_default();
1✔
986

987
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
988
            params: ReprojectionParams {
1✔
989
                target_spatial_reference,
1✔
990
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
991
            },
1✔
992
            sources: SingleRasterOrVectorSource {
1✔
993
                source: lines_source.into(),
1✔
994
            },
1✔
995
        })
1✔
996
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
997
        .await?;
1✔
998

999
        let query_processor = initialized_operator.query_processor()?;
1✔
1000

1001
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
1002

1003
        let query_rectangle = VectorQueryRectangle::new(
1✔
1004
            BoundingBox2D::new(
1✔
1005
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
1006
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
1007
            )
1008
            .unwrap(),
1✔
1009
            TimeInterval::default(),
1✔
1010
            ColumnSelection::all(),
1✔
1011
        );
1012
        let ctx = exe_ctx.mock_query_context(ChunkByteSize::MAX);
1✔
1013

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

1016
        let result = query
1✔
1017
            .map(Result::unwrap)
1✔
1018
            .collect::<Vec<MultiLineStringCollection>>()
1✔
1019
            .await;
1✔
1020

1021
        assert_eq!(result.len(), 1);
1✔
1022

1023
        result[0]
1✔
1024
            .geometries()
1✔
1025
            .zip(expected.iter())
1✔
1026
            .for_each(|(a, e)| {
1✔
1027
                assert!(approx_eq!(
1✔
1028
                    &MultiLineString,
1029
                    &a.into(),
1✔
1030
                    e,
1✔
1031
                    epsilon = 0.00001
1032
                ));
1033
            });
1✔
1034

1035
        Ok(())
2✔
1036
    }
1✔
1037

1038
    #[tokio::test]
1039
    async fn multi_polygons() -> Result<()> {
1✔
1040
        let polygons = MultiPolygonCollection::from_data(
1✔
1041
            vec![
1✔
1042
                MultiPolygon::new(vec![vec![vec![
1✔
1043
                    MARBURG_EPSG_4326,
1044
                    COLOGNE_EPSG_4326,
1045
                    HAMBURG_EPSG_4326,
1046
                    MARBURG_EPSG_4326,
1047
                ]]])
1048
                .unwrap(),
1✔
1049
            ],
1050
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
1051
            Default::default(),
1✔
1052
            CacheHint::default(),
1✔
1053
        )?;
×
1054

1055
        let expected = [MultiPolygon::new(vec![vec![vec![
1✔
1056
            MARBURG_EPSG_900_913,
1✔
1057
            COLOGNE_EPSG_900_913,
1✔
1058
            HAMBURG_EPSG_900_913,
1✔
1059
            MARBURG_EPSG_900_913,
1✔
1060
        ]]])
1✔
1061
        .unwrap()];
1✔
1062

1063
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
1064

1065
        let target_spatial_reference =
1✔
1066
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
1067

1068
        let exe_ctx = MockExecutionContext::test_default();
1✔
1069

1070
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1071
            params: ReprojectionParams {
1✔
1072
                target_spatial_reference,
1✔
1073
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1074
            },
1✔
1075
            sources: SingleRasterOrVectorSource {
1✔
1076
                source: polygon_source.into(),
1✔
1077
            },
1✔
1078
        })
1✔
1079
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1080
        .await?;
1✔
1081

1082
        let query_processor = initialized_operator.query_processor()?;
1✔
1083

1084
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
1085

1086
        let query_rectangle = VectorQueryRectangle::new(
1✔
1087
            BoundingBox2D::new(
1✔
1088
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
1089
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
1090
            )
1091
            .unwrap(),
1✔
1092
            TimeInterval::default(),
1✔
1093
            ColumnSelection::all(),
1✔
1094
        );
1095
        let ctx = exe_ctx.mock_query_context(ChunkByteSize::MAX);
1✔
1096

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

1099
        let result = query
1✔
1100
            .map(Result::unwrap)
1✔
1101
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1102
            .await;
1✔
1103

1104
        assert_eq!(result.len(), 1);
1✔
1105

1106
        result[0]
1✔
1107
            .geometries()
1✔
1108
            .zip(expected.iter())
1✔
1109
            .for_each(|(a, e)| {
1✔
1110
                assert!(approx_eq!(&MultiPolygon, &a.into(), e, epsilon = 0.00001));
1✔
1111
            });
1✔
1112

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

1116
    #[allow(clippy::too_many_lines)]
1117
    #[tokio::test]
1118
    async fn raster_identity() -> Result<()> {
1✔
1119
        let projection = SpatialReference::epsg_4326();
1✔
1120

1121
        let data = vec![
1✔
1122
            RasterTile2D {
1✔
1123
                time: TimeInterval::new_unchecked(0, 5),
1✔
1124
                tile_position: [-1, 0].into(),
1✔
1125
                band: 0,
1✔
1126
                global_geo_transform: TestDefault::test_default(),
1✔
1127
                grid_array: Grid::new([2, 2].into(), vec![1_u8, 2, 3, 4])
1✔
1128
                    .unwrap()
1✔
1129
                    .into(),
1✔
1130
                properties: Default::default(),
1✔
1131
                cache_hint: CacheHint::default(),
1✔
1132
            },
1✔
1133
            RasterTile2D {
1✔
1134
                time: TimeInterval::new_unchecked(0, 5),
1✔
1135
                tile_position: [-1, 1].into(),
1✔
1136
                band: 0,
1✔
1137
                global_geo_transform: TestDefault::test_default(),
1✔
1138
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
1139
                properties: Default::default(),
1✔
1140
                cache_hint: CacheHint::default(),
1✔
1141
            },
1✔
1142
            RasterTile2D {
1✔
1143
                time: TimeInterval::new_unchecked(0, 5),
1✔
1144
                tile_position: [0, 0].into(),
1✔
1145
                band: 0,
1✔
1146
                global_geo_transform: TestDefault::test_default(),
1✔
1147
                grid_array: Grid::new([2, 2].into(), vec![1_u8, 2, 3, 4])
1✔
1148
                    .unwrap()
1✔
1149
                    .into(),
1✔
1150
                properties: Default::default(),
1✔
1151
                cache_hint: CacheHint::default(),
1✔
1152
            },
1✔
1153
            RasterTile2D {
1✔
1154
                time: TimeInterval::new_unchecked(0, 5),
1✔
1155
                tile_position: [0, 1].into(),
1✔
1156
                band: 0,
1✔
1157
                global_geo_transform: TestDefault::test_default(),
1✔
1158
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
1159
                properties: Default::default(),
1✔
1160
                cache_hint: CacheHint::default(),
1✔
1161
            },
1✔
1162
            RasterTile2D {
1✔
1163
                time: TimeInterval::new_unchecked(5, 10),
1✔
1164
                tile_position: [-1, 0].into(),
1✔
1165
                band: 0,
1✔
1166
                global_geo_transform: TestDefault::test_default(),
1✔
1167
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
1168
                    .unwrap()
1✔
1169
                    .into(),
1✔
1170
                properties: Default::default(),
1✔
1171
                cache_hint: CacheHint::default(),
1✔
1172
            },
1✔
1173
            RasterTile2D {
1✔
1174
                time: TimeInterval::new_unchecked(5, 10),
1✔
1175
                tile_position: [-1, 1].into(),
1✔
1176
                band: 0,
1✔
1177
                global_geo_transform: TestDefault::test_default(),
1✔
1178
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
1179
                    .unwrap()
1✔
1180
                    .into(),
1✔
1181
                properties: Default::default(),
1✔
1182
                cache_hint: CacheHint::default(),
1✔
1183
            },
1✔
1184
            RasterTile2D {
1✔
1185
                time: TimeInterval::new_unchecked(5, 10),
1✔
1186
                tile_position: [0, 0].into(),
1✔
1187
                band: 0,
1✔
1188
                global_geo_transform: TestDefault::test_default(),
1✔
1189
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
1190
                    .unwrap()
1✔
1191
                    .into(),
1✔
1192
                properties: Default::default(),
1✔
1193
                cache_hint: CacheHint::default(),
1✔
1194
            },
1✔
1195
            RasterTile2D {
1✔
1196
                time: TimeInterval::new_unchecked(5, 10),
1✔
1197
                tile_position: [0, 1].into(),
1✔
1198
                band: 0,
1✔
1199
                global_geo_transform: TestDefault::test_default(),
1✔
1200
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
1201
                    .unwrap()
1✔
1202
                    .into(),
1✔
1203
                properties: Default::default(),
1✔
1204
                cache_hint: CacheHint::default(),
1✔
1205
            },
1✔
1206
        ];
1207

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

1210
        let result_descriptor = RasterResultDescriptor {
1✔
1211
            data_type: RasterDataType::U8,
1✔
1212
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1213
            time: crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
1214
                Some(TimeInterval::new_unchecked(0, 10)),
1✔
1215
                TimeStep::millis(5),
1✔
1216
            ),
1✔
1217
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1218
                geo_transform,
1✔
1219
                GridBoundingBox2D::new([-2, 0], [1, 3]).unwrap(),
1✔
1220
            ),
1✔
1221
            bands: RasterBandDescriptors::new_single_band(),
1✔
1222
        };
1✔
1223

1224
        let exe_ctx =
1✔
1225
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
1226

1227
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1228

1229
        let mrs1 = MockRasterSource {
1✔
1230
            params: MockRasterSourceParams {
1✔
1231
                data: data.clone(),
1✔
1232
                result_descriptor,
1✔
1233
            },
1✔
1234
        }
1✔
1235
        .boxed();
1✔
1236

1237
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1238
            params: ReprojectionParams {
1✔
1239
                target_spatial_reference: projection, // This test will do a identity reprojection
1✔
1240
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1241
            },
1✔
1242
            sources: SingleRasterOrVectorSource {
1✔
1243
                source: mrs1.into(),
1✔
1244
            },
1✔
1245
        })
1✔
1246
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1247
        .await?;
1✔
1248

1249
        let qp = initialized_operator
1✔
1250
            .query_processor()
1✔
1251
            .unwrap()
1✔
1252
            .get_u8()
1✔
1253
            .unwrap();
1✔
1254

1255
        let query_rect = RasterQueryRectangle::new(
1✔
1256
            GridBoundingBox2D::new([-2, 0], [1, 3]).unwrap(),
1✔
1257
            TimeInterval::new_unchecked(0, 10),
1✔
1258
            BandSelection::first(),
1✔
1259
        );
1260

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

1263
        let res = a
1✔
1264
            .map(Result::unwrap)
1✔
1265
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1266
            .await;
1✔
1267
        assert!(data.tiles_equal_ignoring_cache_hint(&res));
1✔
1268

1269
        Ok(())
2✔
1270
    }
1✔
1271

1272
    #[tokio::test]
1273
    async fn raster_ndvi_3857() -> Result<()> {
1✔
1274
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1275
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1276

1277
        let tile_size = GridShape2D::new_2d(512, 512);
1✔
1278
        exe_ctx.tiling_specification = TilingSpecification::new(tile_size);
1✔
1279

1280
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1281

1282
        let time_interval = TimeInterval::new_unchecked(1_396_303_200_000, 1_396_389_600_000);
1✔
1283
        // 2014-04-01
1284

1285
        let gdal_op = GdalSource {
1✔
1286
            params: GdalSourceParameters::new(id.clone()),
1✔
1287
        }
1✔
1288
        .boxed();
1✔
1289

1290
        let projection = SpatialReference::new(
1✔
1291
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
1292
            3857,
1293
        );
1294

1295
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1296
            params: ReprojectionParams {
1✔
1297
                target_spatial_reference: projection,
1✔
1298
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1299
            },
1✔
1300
            sources: SingleRasterOrVectorSource {
1✔
1301
                source: gdal_op.into(),
1✔
1302
            },
1✔
1303
        })
1✔
1304
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1305
        .await?;
1✔
1306

1307
        let qp = initialized_operator
1✔
1308
            .query_processor()
1✔
1309
            .unwrap()
1✔
1310
            .get_u8()
1✔
1311
            .unwrap();
1✔
1312

1313
        let result_descritptor = qp.result_descriptor();
1✔
1314

1315
        assert_approx_eq!(
1✔
1316
            f64,
1317
            14_255.015_508_816_849, // TODO: GDAL output is 14228.560819126376373
1318
            result_descritptor
1✔
1319
                .spatial_grid_descriptor()
1✔
1320
                .spatial_resolution()
1✔
1321
                .x,
1322
            epsilon = 0.000_001
1323
        );
1324

1325
        assert_approx_eq!(
1✔
1326
            f64,
1327
            14_255.015_508_816_849, // TODO: GDAL output is -14233.615370039031404
1328
            result_descritptor
1✔
1329
                .spatial_grid_descriptor()
1✔
1330
                .spatial_resolution()
1✔
1331
                .y,
1332
            epsilon = 0.000_001
1333
        );
1334

1335
        let tlz = result_descritptor
1✔
1336
            .spatial_grid_descriptor()
1✔
1337
            .tiling_grid_definition(query_ctx.tiling_specification())
1✔
1338
            .generate_data_tiling_strategy();
1✔
1339
        let query_tl_pixel = tlz.tile_idx_to_global_pixel_idx([-1, 0].into());
1✔
1340
        let query_bounds =
1✔
1341
            GridBoundingBox2D::new(query_tl_pixel, query_tl_pixel + [511, 511]).unwrap();
1✔
1342

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

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

1347
        let res = qs
1✔
1348
            .map(Result::unwrap)
1✔
1349
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1350
            .await;
1✔
1351

1352
        // get the worldfile
1353
        // println!("{}", res[0].tile_geo_transform().worldfile_string());
1354

1355
        // Write the tile to a file
1356

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

1360
        std::io::Write::write(
1361
            &mut buffer,
1362
            res[0]
1363
                .clone()
1364
                .into_materialized_tile()
1365
                .grid_array
1366
                .inner_grid
1367
                .data
1368
                .as_slice(),
1369
        )?;
1370
        */
1371

1372
        // This check is against a tile produced by the operator itself. It was visually validated. TODO: rebuild when open issues are solved.
1373
        // A perfect validation would be against a GDAL output generated like this:
1374
        // 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
1375

1376
        assert_eq!(
1✔
1377
            include_bytes!(
1378
                "../../../test_data/raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01_tile-20_v6.rst"
1379
            ) as &[u8],
1✔
1380
            res[0]
1✔
1381
                .clone()
1✔
1382
                .into_materialized_tile()
1✔
1383
                .grid_array
1✔
1384
                .inner_grid
1✔
1385
                .data
1✔
1386
                .as_slice()
1✔
1387
        );
1388

1389
        Ok(())
2✔
1390
    }
1✔
1391

1392
    #[test]
1393
    fn query_rewrite_4326_3857() {
1✔
1394
        let query = VectorQueryRectangle::new(
1✔
1395
            BoundingBox2D::new_unchecked((-180., -90.).into(), (180., 90.).into()),
1✔
1396
            TimeInterval::default(),
1✔
1397
            ColumnSelection::all(),
1✔
1398
        );
1399

1400
        let expected = BoundingBox2D::new_unchecked(
1✔
1401
            (-20_037_508.342_789_244, -20_048_966.104_014_594).into(),
1✔
1402
            (20_037_508.342_789_244, 20_048_966.104_014_594).into(),
1✔
1403
        );
1404

1405
        let reprojected = reproject_spatial_query(
1✔
1406
            query.spatial_bounds(),
1✔
1407
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857),
1✔
1408
            SpatialReference::epsg_4326(),
1✔
1409
            true,
1410
        )
1411
        .unwrap()
1✔
1412
        .unwrap();
1✔
1413

1414
        assert!(approx_eq!(
1✔
1415
            BoundingBox2D,
1416
            expected,
1✔
1417
            reprojected,
1✔
1418
            epsilon = 0.000_001
1419
        ));
1420
    }
1✔
1421

1422
    #[allow(clippy::too_many_lines)]
1423
    #[tokio::test]
1424
    async fn raster_ndvi_3857_to_4326() -> Result<()> {
1✔
1425
        let tile_size_in_pixels = [200, 200].into();
1✔
1426
        let data_geo_transform = GeoTransform::new(
1✔
1427
            Coordinate2D::new(-20_037_508.342_789_244, 19_971_868.880_408_562),
1✔
1428
            14_052.950_258_048_738,
1429
            -14_057.881_117_788_405,
1430
        );
1431
        let data_bounds = GridBoundingBox2D::new([0, 0], [2840, 2850]).unwrap();
1✔
1432
        let result_descriptor = RasterResultDescriptor {
1✔
1433
            data_type: RasterDataType::U8,
1✔
1434
            spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857).into(),
1✔
1435
            time: crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
1436
                Some(TimeInterval::new_unchecked(
1✔
1437
                    TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1438
                    TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1439
                )),
1✔
1440
                TimeStep::months(1),
1✔
1441
            ),
1✔
1442
            spatial_grid: SpatialGridDescriptor::source_from_parts(data_geo_transform, data_bounds),
1✔
1443
            bands: RasterBandDescriptors::new_single_band(),
1✔
1444
        };
1✔
1445

1446
        let m = GdalMetaDataRegular {
1✔
1447
            data_time: TimeInterval::new_unchecked(
1✔
1448
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1449
                TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1450
            ),
1✔
1451
            step: TimeStep {
1✔
1452
                granularity: TimeGranularity::Months,
1✔
1453
                step: 1,
1✔
1454
            },
1✔
1455
            time_placeholders: hashmap! {
1✔
1456
                "%_START_TIME_%".to_string() => GdalSourceTimePlaceholder {
1✔
1457
                    format: DateTimeParseFormat::custom("%Y-%m-%d".to_string()),
1✔
1458
                    reference: TimeReference::Start,
1✔
1459
                },
1✔
1460
            },
1✔
1461
            params: GdalDatasetParameters {
1✔
1462
                file_path: test_data!(
1✔
1463
                    "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_%_START_TIME_%.TIFF"
1✔
1464
                )
1✔
1465
                .into(),
1✔
1466
                rasterband_channel: 1,
1✔
1467
                geo_transform: GdalDatasetGeoTransform {
1✔
1468
                    origin_coordinate: data_geo_transform.origin_coordinate,
1✔
1469
                    x_pixel_size: data_geo_transform.x_pixel_size(),
1✔
1470
                    y_pixel_size: data_geo_transform.y_pixel_size(),
1✔
1471
                },
1✔
1472
                width: data_bounds.axis_size_x(),
1✔
1473
                height: data_bounds.axis_size_y(),
1✔
1474
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1475
                no_data_value: Some(0.),
1✔
1476
                properties_mapping: None,
1✔
1477
                gdal_open_options: None,
1✔
1478
                gdal_config_options: None,
1✔
1479
                allow_alphaband_as_mask: true,
1✔
1480
                retry: None,
1✔
1481
            },
1✔
1482
            result_descriptor: result_descriptor.clone(),
1✔
1483
            cache_ttl: CacheTtlSeconds::default(),
1✔
1484
        };
1✔
1485

1486
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1487
            tile_size_in_pixels,
1✔
1488
        ));
1489

1490
        let id: DataId = DatasetId::new().into();
1✔
1491
        let name = NamedData::with_system_name("ndvi");
1✔
1492
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1493

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

1496
        let gdal_op = GdalSource {
1✔
1497
            params: GdalSourceParameters::new(name),
1✔
1498
        }
1✔
1499
        .boxed();
1✔
1500

1501
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1502
            params: ReprojectionParams {
1✔
1503
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1504
                derive_out_spec: DeriveOutRasterSpecsSource::DataBounds,
1✔
1505
            },
1✔
1506
            sources: SingleRasterOrVectorSource {
1✔
1507
                source: gdal_op.into(),
1✔
1508
            },
1✔
1509
        })
1✔
1510
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1511
        .await?;
1✔
1512

1513
        let qp = initialized_operator
1✔
1514
            .query_processor()
1✔
1515
            .unwrap()
1✔
1516
            .get_u8()
1✔
1517
            .unwrap();
1✔
1518

1519
        let qr = qp.result_descriptor();
1✔
1520
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1521

1522
        let qs = qp
1✔
1523
            .raster_query(
1✔
1524
                RasterQueryRectangle::new(
1✔
1525
                    qr.spatial_grid_descriptor()
1✔
1526
                        .tiling_grid_definition(query_ctx.tiling_specification())
1✔
1527
                        .tiling_grid_bounds(),
1✔
1528
                    time_interval,
1✔
1529
                    BandSelection::first(),
1✔
1530
                ),
1✔
1531
                &query_ctx,
1✔
1532
            )
1✔
1533
            .await
1✔
1534
            .unwrap();
1✔
1535

1536
        let tiles = qs
1✔
1537
            .map(Result::unwrap)
1✔
1538
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1539
            .await;
1✔
1540

1541
        // 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
1542
        assert_eq!(tiles.len(), /*18*/ 20 * 10);
1✔
1543

1544
        // none of the tiles should be empty
1545
        assert!(tiles.iter().all(|t| !t.is_empty()));
200✔
1546
        Ok(())
2✔
1547
    }
1✔
1548

1549
    #[tokio::test]
1550
    async fn query_outside_projection_area_of_use_produces_empty_tiles() {
1✔
1551
        let tile_size_in_pixels = [600, 600].into();
1✔
1552
        let result_descriptor = RasterResultDescriptor {
1✔
1553
            data_type: RasterDataType::U8,
1✔
1554
            spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636).into(),
1✔
1555
            time: crate::engine::TimeDescriptor::new_irregular(None),
1✔
1556
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1557
                GeoTransform::new(
1✔
1558
                    Coordinate2D::new(166_021.44, 9_329_005.188),
1✔
1559
                    534_994.66 - 166_021.444,
1✔
1560
                    -9_329_005.18,
1✔
1561
                ),
1✔
1562
                GridBoundingBox2D::new_min_max(0, 100, 0, 100).unwrap(),
1✔
1563
            ),
1✔
1564
            bands: RasterBandDescriptors::new_single_band(),
1✔
1565
        };
1✔
1566

1567
        let m = GdalMetaDataStatic {
1✔
1568
            time: Some(TimeInterval::default()),
1✔
1569
            params: GdalDatasetParameters {
1✔
1570
                file_path: PathBuf::new(),
1✔
1571
                rasterband_channel: 1,
1✔
1572
                geo_transform: GdalDatasetGeoTransform {
1✔
1573
                    origin_coordinate: (166_021.44, 9_329_005.188).into(),
1✔
1574
                    x_pixel_size: (534_994.66 - 166_021.444) / 100.,
1✔
1575
                    y_pixel_size: -9_329_005.18 / 100.,
1✔
1576
                },
1✔
1577
                width: 100,
1✔
1578
                height: 100,
1✔
1579
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1580
                no_data_value: Some(0.),
1✔
1581
                properties_mapping: None,
1✔
1582
                gdal_open_options: None,
1✔
1583
                gdal_config_options: None,
1✔
1584
                allow_alphaband_as_mask: true,
1✔
1585
                retry: None,
1✔
1586
            },
1✔
1587
            result_descriptor: result_descriptor.clone(),
1✔
1588
            cache_ttl: CacheTtlSeconds::default(),
1✔
1589
        };
1✔
1590

1591
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1592
            tile_size_in_pixels,
1✔
1593
        ));
1594
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1595

1596
        let id: DataId = DatasetId::new().into();
1✔
1597
        let name = NamedData::with_system_name("ndvi");
1✔
1598
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1599

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

1602
        let gdal_op = GdalSource {
1✔
1603
            params: GdalSourceParameters::new(name),
1✔
1604
        }
1✔
1605
        .boxed();
1✔
1606

1607
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1608
            params: ReprojectionParams {
1✔
1609
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1610
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1611
            },
1✔
1612
            sources: SingleRasterOrVectorSource {
1✔
1613
                source: gdal_op.into(),
1✔
1614
            },
1✔
1615
        })
1✔
1616
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1617
        .await
1✔
1618
        .unwrap();
1✔
1619

1620
        let qp = initialized_operator
1✔
1621
            .query_processor()
1✔
1622
            .unwrap()
1✔
1623
            .get_u8()
1✔
1624
            .unwrap();
1✔
1625

1626
        let result = qp
1✔
1627
            .raster_query(
1✔
1628
                RasterQueryRectangle::new(
1✔
1629
                    GridBoundingBox2D::new_min_max(500, 1000, 500, 1000).unwrap(),
1✔
1630
                    time_interval,
1✔
1631
                    BandSelection::first(),
1✔
1632
                ),
1✔
1633
                &query_ctx,
1✔
1634
            )
1✔
1635
            .await
1✔
1636
            .unwrap()
1✔
1637
            .map(Result::unwrap)
1✔
1638
            .collect::<Vec<_>>()
1✔
1639
            .await;
1✔
1640

1641
        assert_eq!(result.len(), 4);
1✔
1642

1643
        for r in result {
5✔
1644
            assert!(r.is_empty());
4✔
1645
        }
1✔
1646
    }
1✔
1647

1648
    #[tokio::test]
1649
    async fn points_from_wgs84_to_utm36n() {
1✔
1650
        let exe_ctx = MockExecutionContext::test_default();
1✔
1651
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1652

1653
        let point_source = MockFeatureCollectionSource::single(
1✔
1654
            MultiPointCollection::from_data(
1✔
1655
                MultiPoint::many(vec![
1✔
1656
                    vec![(30.0, 0.0)], // lower left of utm36n area of use
1✔
1657
                    vec![(36.0, 84.0)],
1✔
1658
                    vec![(33.0, 42.0)], // upper right of utm36n area of use
1✔
1659
                ])
1660
                .unwrap(),
1✔
1661
                vec![TimeInterval::default(); 3],
1✔
1662
                HashMap::default(),
1✔
1663
                CacheHint::default(),
1✔
1664
            )
1665
            .unwrap(),
1✔
1666
        )
1667
        .boxed();
1✔
1668

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

1685
        let qp = initialized_operator
1✔
1686
            .query_processor()
1✔
1687
            .unwrap()
1✔
1688
            .multi_point()
1✔
1689
            .unwrap();
1✔
1690

1691
        let spatial_bounds = BoundingBox2D::new(
1✔
1692
            (166_021.44, 0.00).into(), // lower left of projected utm36n area of use
1✔
1693
            (534_994.666_6, 9_329_005.18).into(), // upper right of projected utm36n area of use
1✔
1694
        )
1695
        .unwrap();
1✔
1696

1697
        let qs = qp
1✔
1698
            .vector_query(
1✔
1699
                VectorQueryRectangle::new(
1✔
1700
                    spatial_bounds,
1✔
1701
                    TimeInterval::default(),
1✔
1702
                    ColumnSelection::all(),
1✔
1703
                ),
1✔
1704
                &query_ctx,
1✔
1705
            )
1✔
1706
            .await
1✔
1707
            .unwrap();
1✔
1708

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

1711
        assert_eq!(points.len(), 1);
1✔
1712

1713
        let points = &points[0];
1✔
1714

1715
        assert_eq!(
1✔
1716
            points.coordinates(),
1✔
1717
            &[
1✔
1718
                (166_021.443_080_538_42, 0.0).into(),
1✔
1719
                (534_994.655_061_136_1, 9_329_005.182_447_437).into(),
1✔
1720
                (499_999.999_999_999_5, 4_649_776.224_819_178).into()
1✔
1721
            ]
1✔
1722
        );
1✔
1723
    }
1✔
1724

1725
    #[tokio::test]
1726
    async fn points_from_utm36n_to_wgs84() {
1✔
1727
        let exe_ctx = MockExecutionContext::test_default();
1✔
1728
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1729

1730
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1731
            vec![
1✔
1732
                MultiPointCollection::from_data(
1✔
1733
                    MultiPoint::many(vec![
1✔
1734
                        vec![(166_021.443_080_538_42, 0.0)],
1✔
1735
                        vec![(534_994.655_061_136_1, 9_329_005.182_447_437)],
1✔
1736
                        vec![(499_999.999_999_999_5, 4_649_776.224_819_178)],
1✔
1737
                    ])
1738
                    .unwrap(),
1✔
1739
                    vec![TimeInterval::default(); 3],
1✔
1740
                    HashMap::default(),
1✔
1741
                    CacheHint::default(),
1✔
1742
                )
1743
                .unwrap(),
1✔
1744
            ],
1745
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1746
        )
1747
        .boxed();
1✔
1748

1749
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1750
            params: ReprojectionParams {
1✔
1751
                target_spatial_reference: SpatialReference::new(
1✔
1752
                    SpatialReferenceAuthority::Epsg,
1✔
1753
                    4326, // utm36n
1✔
1754
                ),
1✔
1755
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1756
            },
1✔
1757
            sources: SingleRasterOrVectorSource {
1✔
1758
                source: point_source.into(),
1✔
1759
            },
1✔
1760
        })
1✔
1761
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1762
        .await
1✔
1763
        .unwrap();
1✔
1764

1765
        let qp = initialized_operator
1✔
1766
            .query_processor()
1✔
1767
            .unwrap()
1✔
1768
            .multi_point()
1✔
1769
            .unwrap();
1✔
1770

1771
        let spatial_bounds = BoundingBox2D::new(
1✔
1772
            (30.0, 0.0).into(),  // lower left of utm36n area of use
1✔
1773
            (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1774
        )
1775
        .unwrap();
1✔
1776

1777
        let qs = qp
1✔
1778
            .vector_query(
1✔
1779
                VectorQueryRectangle::new(
1✔
1780
                    spatial_bounds,
1✔
1781
                    TimeInterval::default(),
1✔
1782
                    ColumnSelection::all(),
1✔
1783
                ),
1✔
1784
                &query_ctx,
1✔
1785
            )
1✔
1786
            .await
1✔
1787
            .unwrap();
1✔
1788

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

1791
        assert_eq!(points.len(), 1);
1✔
1792

1793
        let points = &points[0];
1✔
1794

1795
        assert!(approx_eq!(
1✔
1796
            &[Coordinate2D],
1✔
1797
            points.coordinates(),
1✔
1798
            &[
1✔
1799
                (30.0, 0.0).into(), // lower left of utm36n area of use
1✔
1800
                (36.0, 84.0).into(),
1✔
1801
                (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1802
            ]
1✔
1803
        ));
1✔
1804
    }
1✔
1805

1806
    #[tokio::test]
1807
    async fn points_from_utm36n_to_wgs84_out_of_area() {
1✔
1808
        // TODO this test becomes obsolete in this form, it should be removed or we test for 'correct' reprojection here
1809

1810
        // 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
1811

1812
        let exe_ctx = MockExecutionContext::test_default();
1✔
1813
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1814

1815
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1816
            vec![
1✔
1817
                MultiPointCollection::from_data(
1✔
1818
                    MultiPoint::many(vec![
1✔
1819
                        vec![(758_565., 4_928_353.)], // (12.25, 44,46)
1✔
1820
                    ])
1821
                    .unwrap(),
1✔
1822
                    vec![TimeInterval::default(); 1],
1✔
1823
                    HashMap::default(),
1✔
1824
                    CacheHint::default(),
1✔
1825
                )
1826
                .unwrap(),
1✔
1827
            ],
1828
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1829
        )
1830
        .boxed();
1✔
1831

1832
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1833
            params: ReprojectionParams {
1✔
1834
                target_spatial_reference: SpatialReference::new(
1✔
1835
                    SpatialReferenceAuthority::Epsg,
1✔
1836
                    4326, // utm36n
1✔
1837
                ),
1✔
1838
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1839
            },
1✔
1840
            sources: SingleRasterOrVectorSource {
1✔
1841
                source: point_source.into(),
1✔
1842
            },
1✔
1843
        })
1✔
1844
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1845
        .await
1✔
1846
        .unwrap();
1✔
1847

1848
        let qp = initialized_operator
1✔
1849
            .query_processor()
1✔
1850
            .unwrap()
1✔
1851
            .multi_point()
1✔
1852
            .unwrap();
1✔
1853

1854
        let spatial_bounds = BoundingBox2D::new(
1✔
1855
            (10.0, 0.0).into(),  // -20 x values left of lower left of utm36n area of use
1✔
1856
            (13.0, 42.0).into(), // -20 x values left of upper right of utm36n area of use
1✔
1857
        )
1858
        .unwrap();
1✔
1859

1860
        let qs = qp
1✔
1861
            .vector_query(
1✔
1862
                VectorQueryRectangle::new(
1✔
1863
                    spatial_bounds,
1✔
1864
                    TimeInterval::default(),
1✔
1865
                    ColumnSelection::all(),
1✔
1866
                ),
1✔
1867
                &query_ctx,
1✔
1868
            )
1✔
1869
            .await
1✔
1870
            .unwrap();
1✔
1871

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

1874
        assert_eq!(points.len(), 1);
1✔
1875

1876
        //let points = &points[0];
1877

1878
        //assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1879
        //assert!(points.coordinates().is_empty());
1880
    }
1✔
1881

1882
    /* TODO resolve the problem with empty intersections
1883
     */
1884
    #[test]
1885
    fn it_derives_raster_result_descriptor() {
1✔
1886
        let in_proj = SpatialReference::epsg_4326();
1✔
1887
        let out_proj = SpatialReference::from_str("EPSG:3857").unwrap();
1✔
1888

1889
        let geo_transform = GeoTransform::new(Coordinate2D::new(0., 0.), 0.1, -0.1);
1✔
1890
        let grid_bounds = GridBoundingBox2D::new_min_max(-850, 849, -1800, 1799).unwrap();
1✔
1891
        let spatial_grid = SpatialGridDefinition::new(geo_transform, grid_bounds);
1✔
1892

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

1895
        let out_spatial_grid = spatial_grid.reproject(&projector).unwrap();
1✔
1896

1897
        assert_eq!(
1✔
1898
            out_spatial_grid.geo_transform.origin_coordinate(),
1✔
1899
            Coordinate2D::new(0., 0.)
1✔
1900
        );
1901

1902
        assert_eq!(
1✔
1903
            out_spatial_grid.geo_transform.spatial_resolution(),
1✔
1904
            SpatialResolution::new_unchecked(14_212.246_793_017_477, 14_212.246_793_017_477)
1✔
1905
        );
1906

1907
        /*
1908
        Projected bounds:
1909
        -20037508.34 -20048966.1
1910
        20037508.34 20048966.1
1911
        */
1912

1913
        assert_eq!(
1✔
1914
            out_spatial_grid.grid_bounds,
1915
            GridBoundingBox2D::new_min_max(-1405, 1405, -1410, 1409).unwrap()
1✔
1916
        );
1917
    }
1✔
1918
}
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