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

geo-engine / geoengine / 19438548963

17 Nov 2025 05:27PM UTC coverage: 88.512%. First build
19438548963

Pull #1083

github

web-flow
Merge c3ef76798 into f0e6b6470
Pull Request #1083: feat: new gdal source workflow optimization

6329 of 7650 new or added lines in 78 files covered. (82.73%)

116305 of 131400 relevant lines covered (88.51%)

494085.33 hits per line

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

87.51
/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
#[derive(Default)]
49
pub enum DeriveOutRasterSpecsSource {
50
    DataBounds,
51
    #[default]
52
    ProjectionBounds,
53
}
54

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

63
pub type Reprojection = Operator<ReprojectionParams, SingleRasterOrVectorSource>;
64

65
impl Reprojection {}
66

67
impl OperatorName for Reprojection {
68
    const TYPE_NAME: &'static str = "Reprojection";
69
}
70

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

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

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

107
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
8✔
108
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
8✔
109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

254
        let initialized_source = vector_source
255
            .initialize_sources(path.clone(), context)
256
            .await?;
257

258
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
259
            name,
260
            path,
261
            self.params,
262
            initialized_source.vector,
263
        )?;
264

265
        Ok(initialized_operator.boxed())
266
    }
9✔
267

268
    span_fn!(Reprojection);
269
}
270

271
impl InitializedVectorOperator for InitializedVectorReprojection {
272
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
273
        &self.result_descriptor
×
274
    }
×
275

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

332
    fn canonic_name(&self) -> CanonicOperatorName {
×
333
        self.name.clone()
×
334
    }
×
335

336
    fn name(&self) -> &'static str {
×
337
        Reprojection::TYPE_NAME
×
338
    }
×
339

340
    fn path(&self) -> WorkflowOperatorPath {
×
341
        self.path.clone()
×
342
    }
×
343

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

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

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

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

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

415
        let rewritten_query = rewritten_spatial_query.map(|rwq| query.select_spatial_bounds(rwq));
6✔
416

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

436
    fn result_descriptor(&self) -> &VectorResultDescriptor {
12✔
437
        &self.result_descriptor
12✔
438
    }
12✔
439
}
440

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

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

459
        let initialized_source = raster_source
460
            .initialize_sources(path.clone(), context)
461
            .await?;
462

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

471
        Ok(initialized_operator.boxed())
472
    }
7✔
473

474
    span_fn!(Reprojection);
475
}
476

477
impl<O: InitializedRasterOperator> InitializedRasterOperator for InitializedRasterReprojection<O> {
478
    fn result_descriptor(&self) -> &RasterResultDescriptor {
4✔
479
        &self.result_descriptor
4✔
480
    }
4✔
481

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

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

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

612
    fn canonic_name(&self) -> CanonicOperatorName {
×
613
        self.name.clone()
×
614
    }
×
615

616
    fn name(&self) -> &'static str {
×
617
        Reprojection::TYPE_NAME
×
618
    }
×
619

620
    fn path(&self) -> WorkflowOperatorPath {
×
621
        self.path.clone()
×
622
    }
×
623

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

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

634
        self.ensure_resolution_is_compatible_for_optimization(target_resolution)?;
1✔
635

636
        let current_resolution = self.result_descriptor.spatial_grid.spatial_resolution();
1✔
637

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

649
        let input_descriptor = self.source.result_descriptor();
1✔
650

651
        let input_resolution = input_descriptor.spatial_grid.spatial_resolution();
1✔
652
        let scaled_input_resolution = input_resolution * f64::from(output_overview_level_factor);
1✔
653

654
        let source_optimized = self.source.optimize(scaled_input_resolution)?;
1✔
655

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

662
        // TODO: check if these are the correct bounds
663
        let optimized_spatial_bounds = optimized_spatial_grid_descriptor.spatial_partition();
1✔
664

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

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

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

688
        if output_spatial_resolution == target_resolution {
1✔
NEW
689
            return Ok(optimized_reprojection);
×
690
        }
1✔
691

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

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

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

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

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

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

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

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

787
        let tiling_strat = self
788
            .result_descriptor
789
            .spatial_grid_descriptor()
790
            .tiling_grid_definition(ctx.tiling_specification())
791
            .generate_data_tiling_strategy();
792

793
        let time_stream = self.time_query(query.time_interval(), ctx).await?;
794

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

807
    fn result_descriptor(&self) -> &RasterResultDescriptor {
14✔
808
        &self.result_descriptor
14✔
809
    }
14✔
810
}
811

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

820
    async fn _time_query<'a>(
821
        &'a self,
822
        query: geoengine_datatypes::primitives::TimeInterval,
823
        ctx: &'a dyn QueryContext,
824
    ) -> Result<BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>> {
1✔
825
        self.source.time_query(query, ctx).await
826
    }
1✔
827
}
828

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

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

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

901
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
902

903
        let target_spatial_reference =
1✔
904
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
905

906
        let exe_ctx = MockExecutionContext::test_default();
1✔
907

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

920
        let query_processor = initialized_operator.query_processor()?;
1✔
921

922
        let query_processor = query_processor.multi_point().unwrap();
1✔
923

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

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

937
        let result = query
1✔
938
            .map(Result::unwrap)
1✔
939
            .collect::<Vec<MultiPointCollection>>()
1✔
940
            .await;
1✔
941

942
        assert_eq!(result.len(), 1);
1✔
943

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

951
        Ok(())
2✔
952
    }
1✔
953

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

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

977
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
978

979
        let target_spatial_reference =
1✔
980
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
981

982
        let exe_ctx = MockExecutionContext::test_default();
1✔
983

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

996
        let query_processor = initialized_operator.query_processor()?;
1✔
997

998
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
999

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

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

1013
        let result = query
1✔
1014
            .map(Result::unwrap)
1✔
1015
            .collect::<Vec<MultiLineStringCollection>>()
1✔
1016
            .await;
1✔
1017

1018
        assert_eq!(result.len(), 1);
1✔
1019

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

1032
        Ok(())
2✔
1033
    }
1✔
1034

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

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

1060
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
1061

1062
        let target_spatial_reference =
1✔
1063
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
1064

1065
        let exe_ctx = MockExecutionContext::test_default();
1✔
1066

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

1079
        let query_processor = initialized_operator.query_processor()?;
1✔
1080

1081
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
1082

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

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

1096
        let result = query
1✔
1097
            .map(Result::unwrap)
1✔
1098
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1099
            .await;
1✔
1100

1101
        assert_eq!(result.len(), 1);
1✔
1102

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

1110
        Ok(())
2✔
1111
    }
1✔
1112

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

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

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

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

1221
        let exe_ctx =
1✔
1222
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
1223

1224
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1225

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

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

1246
        let qp = initialized_operator
1✔
1247
            .query_processor()
1✔
1248
            .unwrap()
1✔
1249
            .get_u8()
1✔
1250
            .unwrap();
1✔
1251

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

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

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

1266
        Ok(())
2✔
1267
    }
1✔
1268

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

1274
        let tile_size = GridShape2D::new_2d(512, 512);
1✔
1275
        exe_ctx.tiling_specification = TilingSpecification::new(tile_size);
1✔
1276

1277
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1278

1279
        let time_interval = TimeInterval::new_unchecked(1_396_303_200_000, 1_396_389_600_000);
1✔
1280
        // 2014-04-01
1281

1282
        let gdal_op = GdalSource {
1✔
1283
            params: GdalSourceParameters::new(id.clone()),
1✔
1284
        }
1✔
1285
        .boxed();
1✔
1286

1287
        let projection = SpatialReference::new(
1✔
1288
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
1289
            3857,
1290
        );
1291

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

1304
        let qp = initialized_operator
1✔
1305
            .query_processor()
1✔
1306
            .unwrap()
1✔
1307
            .get_u8()
1✔
1308
            .unwrap();
1✔
1309

1310
        let result_descritptor = qp.result_descriptor();
1✔
1311

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

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

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

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

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

1344
        let res = qs
1✔
1345
            .map(Result::unwrap)
1✔
1346
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1347
            .await;
1✔
1348

1349
        // get the worldfile
1350
        // println!("{}", res[0].tile_geo_transform().worldfile_string());
1351

1352
        // Write the tile to a file
1353

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

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

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

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

1386
        Ok(())
2✔
1387
    }
1✔
1388

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

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

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

1411
        assert!(approx_eq!(
1✔
1412
            BoundingBox2D,
1413
            expected,
1✔
1414
            reprojected,
1✔
1415
            epsilon = 0.000_001
1416
        ));
1417
    }
1✔
1418

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

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

1483
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1484
            tile_size_in_pixels,
1✔
1485
        ));
1486

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

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

1493
        let gdal_op = GdalSource {
1✔
1494
            params: GdalSourceParameters::new(name),
1✔
1495
        }
1✔
1496
        .boxed();
1✔
1497

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

1510
        let qp = initialized_operator
1✔
1511
            .query_processor()
1✔
1512
            .unwrap()
1✔
1513
            .get_u8()
1✔
1514
            .unwrap();
1✔
1515

1516
        let qr = qp.result_descriptor();
1✔
1517
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1518

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

1533
        let tiles = qs
1✔
1534
            .map(Result::unwrap)
1✔
1535
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1536
            .await;
1✔
1537

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

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

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

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

1588
        let mut exe_ctx = MockExecutionContext::new_with_tiling_spec(TilingSpecification::new(
1✔
1589
            tile_size_in_pixels,
1✔
1590
        ));
1591
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1592

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

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

1599
        let gdal_op = GdalSource {
1✔
1600
            params: GdalSourceParameters::new(name),
1✔
1601
        }
1✔
1602
        .boxed();
1✔
1603

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

1617
        let qp = initialized_operator
1✔
1618
            .query_processor()
1✔
1619
            .unwrap()
1✔
1620
            .get_u8()
1✔
1621
            .unwrap();
1✔
1622

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

1638
        assert_eq!(result.len(), 4);
1✔
1639

1640
        for r in result {
5✔
1641
            assert!(r.is_empty());
4✔
1642
        }
1✔
1643
    }
1✔
1644

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

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

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

1682
        let qp = initialized_operator
1✔
1683
            .query_processor()
1✔
1684
            .unwrap()
1✔
1685
            .multi_point()
1✔
1686
            .unwrap();
1✔
1687

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

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

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

1708
        assert_eq!(points.len(), 1);
1✔
1709

1710
        let points = &points[0];
1✔
1711

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

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

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

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

1762
        let qp = initialized_operator
1✔
1763
            .query_processor()
1✔
1764
            .unwrap()
1✔
1765
            .multi_point()
1✔
1766
            .unwrap();
1✔
1767

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

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

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

1788
        assert_eq!(points.len(), 1);
1✔
1789

1790
        let points = &points[0];
1✔
1791

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

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

1807
        // 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
1808

1809
        let exe_ctx = MockExecutionContext::test_default();
1✔
1810
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1811

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

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

1845
        let qp = initialized_operator
1✔
1846
            .query_processor()
1✔
1847
            .unwrap()
1✔
1848
            .multi_point()
1✔
1849
            .unwrap();
1✔
1850

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

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

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

1871
        assert_eq!(points.len(), 1);
1✔
1872

1873
        //let points = &points[0];
1874

1875
        //assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1876
        //assert!(points.coordinates().is_empty());
1877
    }
1✔
1878

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

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

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

1892
        let out_spatial_grid = spatial_grid.reproject(&projector).unwrap();
1✔
1893

1894
        assert_eq!(
1✔
1895
            out_spatial_grid.geo_transform.origin_coordinate(),
1✔
1896
            Coordinate2D::new(0., 0.)
1✔
1897
        );
1898

1899
        assert_eq!(
1✔
1900
            out_spatial_grid.geo_transform.spatial_resolution(),
1✔
1901
            SpatialResolution::new_unchecked(14_212.246_793_017_477, 14_212.246_793_017_477)
1✔
1902
        );
1903

1904
        /*
1905
        Projected bounds:
1906
        -20037508.34 -20048966.1
1907
        20037508.34 20048966.1
1908
        */
1909

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