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

geo-engine / geoengine / 3929938005

pending completion
3929938005

push

github

GitHub
Merge #713

84930 of 96741 relevant lines covered (87.79%)

79640.1 hits per line

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

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

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

38
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
3✔
39
#[serde(rename_all = "camelCase")]
40
pub struct ReprojectionParams {
41
    pub target_spatial_reference: SpatialReference,
42
}
43

44
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
×
45
pub struct ReprojectionBounds {
46
    valid_in_bounds: SpatialPartition2D,
47
    valid_out_bounds: SpatialPartition2D,
48
}
49

50
pub type Reprojection = Operator<ReprojectionParams, SingleRasterOrVectorSource>;
51

52
impl Reprojection {}
53

54
impl OperatorName for Reprojection {
55
    const TYPE_NAME: &'static str = "Reprojection";
56
}
57

58
pub struct InitializedVectorReprojection {
59
    result_descriptor: VectorResultDescriptor,
60
    source: Box<dyn InitializedVectorOperator>,
61
    source_srs: SpatialReference,
62
    target_srs: SpatialReference,
63
}
64

65
pub struct InitializedRasterReprojection {
66
    result_descriptor: RasterResultDescriptor,
67
    source: Box<dyn InitializedRasterOperator>,
68
    state: Option<ReprojectionBounds>,
69
    source_srs: SpatialReference,
70
    target_srs: SpatialReference,
71
    tiling_spec: TilingSpecification,
72
}
73

74
impl InitializedVectorReprojection {
75
    /// Create a new `InitializedVectorReprojection` instance.
76
    /// The `source` must have the same `SpatialReference` as `source_srs`.
77
    ///
78
    /// # Errors
79
    /// This function errors if the `source`'s `SpatialReference` is `None`.
80
    /// This function errors if the source's bounding box cannot be reprojected to the target's `SpatialReference`.
81
    pub fn try_new_with_input(
6✔
82
        params: ReprojectionParams,
6✔
83
        source_vector_operator: Box<dyn InitializedVectorOperator>,
6✔
84
    ) -> Result<Self> {
6✔
85
        let in_desc: VectorResultDescriptor = source_vector_operator.result_descriptor().clone();
6✔
86

87
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
88
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
89

90
        let bbox = if let Some(bbox) = in_desc.bbox {
6✔
91
            let projector =
×
92
                CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
×
93

94
            bbox.reproject_clipped(&projector)? // TODO: if this is none then we could skip the whole reprojection similar to raster?
×
95
        } else {
96
            None
6✔
97
        };
98

99
        let out_desc = VectorResultDescriptor {
6✔
100
            spatial_reference: params.target_spatial_reference.into(),
6✔
101
            data_type: in_desc.data_type,
6✔
102
            columns: in_desc.columns.clone(),
6✔
103
            time: in_desc.time,
6✔
104
            bbox,
6✔
105
        };
6✔
106

6✔
107
        Ok(InitializedVectorReprojection {
6✔
108
            result_descriptor: out_desc,
6✔
109
            source: source_vector_operator,
6✔
110
            source_srs: in_srs,
6✔
111
            target_srs: params.target_spatial_reference,
6✔
112
        })
6✔
113
    }
6✔
114
}
115

116
impl InitializedRasterReprojection {
117
    pub fn try_new_with_input(
6✔
118
        params: ReprojectionParams,
6✔
119
        source_raster_operator: Box<dyn InitializedRasterOperator>,
6✔
120
        tiling_spec: TilingSpecification,
6✔
121
    ) -> Result<Self> {
6✔
122
        let in_desc: RasterResultDescriptor = source_raster_operator.result_descriptor().clone();
6✔
123

124
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
125
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
126

127
        // calculate the intersection of input and output srs in both coordinate systems
128
        let (in_bounds, out_bounds, out_res) = Self::derive_raster_in_bounds_out_bounds_out_res(
6✔
129
            in_srs,
6✔
130
            params.target_spatial_reference,
6✔
131
            in_desc.resolution,
6✔
132
            in_desc.bbox,
6✔
133
        )?;
6✔
134

135
        let result_descriptor = RasterResultDescriptor {
4✔
136
            spatial_reference: params.target_spatial_reference.into(),
4✔
137
            data_type: in_desc.data_type,
4✔
138
            measurement: in_desc.measurement.clone(),
4✔
139
            time: in_desc.time,
4✔
140
            bbox: out_bounds,
4✔
141
            resolution: out_res,
4✔
142
        };
4✔
143

144
        let state = match (in_bounds, out_bounds) {
4✔
145
            (Some(in_bounds), Some(out_bounds)) => Some(ReprojectionBounds {
4✔
146
                valid_in_bounds: in_bounds,
4✔
147
                valid_out_bounds: out_bounds,
4✔
148
            }),
4✔
149
            _ => None,
×
150
        };
151

152
        Ok(InitializedRasterReprojection {
4✔
153
            result_descriptor,
4✔
154
            source: source_raster_operator,
4✔
155
            state,
4✔
156
            source_srs: in_srs,
4✔
157
            target_srs: params.target_spatial_reference,
4✔
158
            tiling_spec,
4✔
159
        })
4✔
160
    }
6✔
161

162
    fn derive_raster_in_bounds_out_bounds_out_res(
7✔
163
        source_srs: SpatialReference,
7✔
164
        target_srs: SpatialReference,
7✔
165
        source_spatial_resolution: Option<SpatialResolution>,
7✔
166
        source_bbox: Option<SpatialPartition2D>,
7✔
167
    ) -> Result<(
7✔
168
        Option<SpatialPartition2D>,
7✔
169
        Option<SpatialPartition2D>,
7✔
170
        Option<SpatialResolution>,
7✔
171
    )> {
7✔
172
        let (in_bbox, out_bbox) = if let Some(bbox) = source_bbox {
7✔
173
            reproject_and_unify_bbox(bbox, source_srs, target_srs)?
4✔
174
        } else {
175
            // use the parts of the area of use that are valid in both spatial references
176
            let valid_bounds_in = source_srs.area_of_use_intersection(&target_srs)?;
3✔
177
            let valid_bounds_out = target_srs.area_of_use_intersection(&source_srs)?;
3✔
178

179
            (valid_bounds_in, valid_bounds_out)
3✔
180
        };
181

182
        let out_res = match (source_spatial_resolution, in_bbox, out_bbox) {
5✔
183
            (Some(in_res), Some(in_bbox), Some(out_bbox)) => {
3✔
184
                suggest_pixel_size_from_diag_cross_projected(in_bbox, out_bbox, in_res).ok()
3✔
185
            }
186
            _ => None,
2✔
187
        };
188

189
        Ok((in_bbox, out_bbox, out_res))
5✔
190
    }
7✔
191
}
192

193
#[typetag::serde]
1✔
194
#[async_trait]
195
impl VectorOperator for Reprojection {
196
    async fn _initialize(
7✔
197
        self: Box<Self>,
7✔
198
        context: &dyn ExecutionContext,
7✔
199
    ) -> Result<Box<dyn InitializedVectorOperator>> {
7✔
200
        let vector_operator = match self.sources.source {
7✔
201
            RasterOrVectorOperator::Vector(operator) => operator,
6✔
202
            RasterOrVectorOperator::Raster(_) => {
203
                return Err(error::Error::InvalidOperatorType {
1✔
204
                    expected: "Vector".to_owned(),
1✔
205
                    found: "Raster".to_owned(),
1✔
206
                })
1✔
207
            }
208
        };
209

210
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
6✔
211
            self.params,
6✔
212
            vector_operator.initialize(context).await?,
6✔
213
        )?;
×
214

215
        Ok(initialized_operator.boxed())
6✔
216
    }
14✔
217

218
    span_fn!(Reprojection);
×
219
}
220

221
impl InitializedVectorOperator for InitializedVectorReprojection {
222
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
223
        &self.result_descriptor
×
224
    }
×
225

226
    fn query_processor(&self) -> Result<TypedVectorQueryProcessor> {
6✔
227
        let source_srs = self.source_srs;
6✔
228
        let target_srs = self.target_srs;
6✔
229
        match self.source.query_processor()? {
6✔
230
            TypedVectorQueryProcessor::Data(source) => Ok(TypedVectorQueryProcessor::Data(
×
231
                MapQueryProcessor::new(
×
232
                    source,
×
233
                    move |query| reproject_query(query, source_srs, target_srs).map_err(From::from),
×
234
                    (),
×
235
                )
×
236
                .boxed(),
×
237
            )),
×
238
            TypedVectorQueryProcessor::MultiPoint(source) => {
4✔
239
                Ok(TypedVectorQueryProcessor::MultiPoint(
4✔
240
                    VectorReprojectionProcessor::new(source, source_srs, target_srs).boxed(),
4✔
241
                ))
4✔
242
            }
243
            TypedVectorQueryProcessor::MultiLineString(source) => {
1✔
244
                Ok(TypedVectorQueryProcessor::MultiLineString(
1✔
245
                    VectorReprojectionProcessor::new(source, source_srs, target_srs).boxed(),
1✔
246
                ))
1✔
247
            }
248
            TypedVectorQueryProcessor::MultiPolygon(source) => {
1✔
249
                Ok(TypedVectorQueryProcessor::MultiPolygon(
1✔
250
                    VectorReprojectionProcessor::new(source, source_srs, target_srs).boxed(),
1✔
251
                ))
1✔
252
            }
253
        }
254
    }
6✔
255
}
256

257
struct VectorReprojectionProcessor<Q, G>
258
where
259
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
260
{
261
    source: Q,
262
    from: SpatialReference,
263
    to: SpatialReference,
264
}
265

266
impl<Q, G> VectorReprojectionProcessor<Q, G>
267
where
268
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
269
{
270
    pub fn new(source: Q, from: SpatialReference, to: SpatialReference) -> Self {
6✔
271
        Self { source, from, to }
6✔
272
    }
6✔
273
}
274

275
#[async_trait]
276
impl<Q, G> QueryProcessor for VectorReprojectionProcessor<Q, G>
277
where
278
    Q: QueryProcessor<Output = FeatureCollection<G>, SpatialBounds = BoundingBox2D>,
279
    FeatureCollection<G>: Reproject<CoordinateProjector, Out = FeatureCollection<G>>,
280
    G: Geometry + ArrowTyped,
281
{
282
    type Output = FeatureCollection<G>;
283
    type SpatialBounds = BoundingBox2D;
284

285
    async fn _query<'a>(
6✔
286
        &'a self,
6✔
287
        query: VectorQueryRectangle,
6✔
288
        ctx: &'a dyn QueryContext,
6✔
289
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
6✔
290
        let rewritten_query = reproject_query(query, self.from, self.to)?;
6✔
291

292
        if let Some(rewritten_query) = rewritten_query {
6✔
293
            Ok(self
5✔
294
                .source
5✔
295
                .query(rewritten_query, ctx)
5✔
296
                .await?
×
297
                .map(move |collection_result| {
5✔
298
                    collection_result.and_then(|collection| {
5✔
299
                        CoordinateProjector::from_known_srs(self.from, self.to)
5✔
300
                            .and_then(|projector| collection.reproject(projector.as_ref()))
5✔
301
                            .map_err(Into::into)
5✔
302
                    })
5✔
303
                })
5✔
304
                .boxed())
5✔
305
        } else {
306
            let res = Ok(FeatureCollection::empty());
1✔
307
            Ok(Box::pin(stream::once(async { res })))
1✔
308
        }
309
    }
12✔
310
}
311

312
#[typetag::serde]
×
313
#[async_trait]
314
impl RasterOperator for Reprojection {
315
    async fn _initialize(
4✔
316
        self: Box<Self>,
4✔
317
        context: &dyn ExecutionContext,
4✔
318
    ) -> Result<Box<dyn InitializedRasterOperator>> {
4✔
319
        let raster_operator = match self.sources.source {
4✔
320
            RasterOrVectorOperator::Raster(operator) => operator,
4✔
321
            RasterOrVectorOperator::Vector(_) => {
322
                return Err(error::Error::InvalidOperatorType {
×
323
                    expected: "Raster".to_owned(),
×
324
                    found: "Vector".to_owned(),
×
325
                })
×
326
            }
327
        };
328

329
        let initialized_operator = InitializedRasterReprojection::try_new_with_input(
4✔
330
            self.params,
4✔
331
            raster_operator.initialize(context).await?,
4✔
332
            context.tiling_specification(),
4✔
333
        )?;
×
334

335
        Ok(initialized_operator.boxed())
4✔
336
    }
8✔
337

338
    span_fn!(Reprojection);
×
339
}
340

341
impl InitializedRasterOperator for InitializedRasterReprojection {
342
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
343
        &self.result_descriptor
×
344
    }
×
345

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

351
        Ok(match self.result_descriptor.data_type {
4✔
352
            geoengine_datatypes::raster::RasterDataType::U8 => {
353
                let qt = q.get_u8().unwrap();
4✔
354
                TypedRasterQueryProcessor::U8(Box::new(RasterReprojectionProcessor::new(
4✔
355
                    qt,
4✔
356
                    self.source_srs,
4✔
357
                    self.target_srs,
4✔
358
                    self.tiling_spec,
4✔
359
                    self.state,
4✔
360
                )))
4✔
361
            }
362
            geoengine_datatypes::raster::RasterDataType::U16 => {
363
                let qt = q.get_u16().unwrap();
×
364
                TypedRasterQueryProcessor::U16(Box::new(RasterReprojectionProcessor::new(
×
365
                    qt,
×
366
                    self.source_srs,
×
367
                    self.target_srs,
×
368
                    self.tiling_spec,
×
369
                    self.state,
×
370
                )))
×
371
            }
372

373
            geoengine_datatypes::raster::RasterDataType::U32 => {
374
                let qt = q.get_u32().unwrap();
×
375
                TypedRasterQueryProcessor::U32(Box::new(RasterReprojectionProcessor::new(
×
376
                    qt,
×
377
                    self.source_srs,
×
378
                    self.target_srs,
×
379
                    self.tiling_spec,
×
380
                    self.state,
×
381
                )))
×
382
            }
383
            geoengine_datatypes::raster::RasterDataType::U64 => {
384
                let qt = q.get_u64().unwrap();
×
385
                TypedRasterQueryProcessor::U64(Box::new(RasterReprojectionProcessor::new(
×
386
                    qt,
×
387
                    self.source_srs,
×
388
                    self.target_srs,
×
389
                    self.tiling_spec,
×
390
                    self.state,
×
391
                )))
×
392
            }
393
            geoengine_datatypes::raster::RasterDataType::I8 => {
394
                let qt = q.get_i8().unwrap();
×
395
                TypedRasterQueryProcessor::I8(Box::new(RasterReprojectionProcessor::new(
×
396
                    qt,
×
397
                    self.source_srs,
×
398
                    self.target_srs,
×
399
                    self.tiling_spec,
×
400
                    self.state,
×
401
                )))
×
402
            }
403
            geoengine_datatypes::raster::RasterDataType::I16 => {
404
                let qt = q.get_i16().unwrap();
×
405
                TypedRasterQueryProcessor::I16(Box::new(RasterReprojectionProcessor::new(
×
406
                    qt,
×
407
                    self.source_srs,
×
408
                    self.target_srs,
×
409
                    self.tiling_spec,
×
410
                    self.state,
×
411
                )))
×
412
            }
413
            geoengine_datatypes::raster::RasterDataType::I32 => {
414
                let qt = q.get_i32().unwrap();
×
415
                TypedRasterQueryProcessor::I32(Box::new(RasterReprojectionProcessor::new(
×
416
                    qt,
×
417
                    self.source_srs,
×
418
                    self.target_srs,
×
419
                    self.tiling_spec,
×
420
                    self.state,
×
421
                )))
×
422
            }
423
            geoengine_datatypes::raster::RasterDataType::I64 => {
424
                let qt = q.get_i64().unwrap();
×
425
                TypedRasterQueryProcessor::I64(Box::new(RasterReprojectionProcessor::new(
×
426
                    qt,
×
427
                    self.source_srs,
×
428
                    self.target_srs,
×
429
                    self.tiling_spec,
×
430
                    self.state,
×
431
                )))
×
432
            }
433
            geoengine_datatypes::raster::RasterDataType::F32 => {
434
                let qt = q.get_f32().unwrap();
×
435
                TypedRasterQueryProcessor::F32(Box::new(RasterReprojectionProcessor::new(
×
436
                    qt,
×
437
                    self.source_srs,
×
438
                    self.target_srs,
×
439
                    self.tiling_spec,
×
440
                    self.state,
×
441
                )))
×
442
            }
443
            geoengine_datatypes::raster::RasterDataType::F64 => {
444
                let qt = q.get_f64().unwrap();
×
445
                TypedRasterQueryProcessor::F64(Box::new(RasterReprojectionProcessor::new(
×
446
                    qt,
×
447
                    self.source_srs,
×
448
                    self.target_srs,
×
449
                    self.tiling_spec,
×
450
                    self.state,
×
451
                )))
×
452
            }
453
        })
454
    }
4✔
455
}
456

457
pub struct RasterReprojectionProcessor<Q, P>
458
where
459
    Q: RasterQueryProcessor<RasterType = P>,
460
{
461
    source: Q,
462
    from: SpatialReference,
463
    to: SpatialReference,
464
    tiling_spec: TilingSpecification,
465
    state: Option<ReprojectionBounds>,
466
    _phantom_data: PhantomData<P>,
467
}
468

469
impl<Q, P> RasterReprojectionProcessor<Q, P>
470
where
471
    Q: RasterQueryProcessor<RasterType = P>,
472
    P: Pixel,
473
{
474
    pub fn new(
4✔
475
        source: Q,
4✔
476
        from: SpatialReference,
4✔
477
        to: SpatialReference,
4✔
478
        tiling_spec: TilingSpecification,
4✔
479
        state: Option<ReprojectionBounds>,
4✔
480
    ) -> Self {
4✔
481
        Self {
4✔
482
            source,
4✔
483
            from,
4✔
484
            to,
4✔
485
            tiling_spec,
4✔
486
            state,
4✔
487
            _phantom_data: PhantomData,
4✔
488
        }
4✔
489
    }
4✔
490
}
491

492
#[async_trait]
493
impl<Q, P> QueryProcessor for RasterReprojectionProcessor<Q, P>
494
where
495
    Q: QueryProcessor<Output = RasterTile2D<P>, SpatialBounds = SpatialPartition2D>,
496
    P: Pixel,
497
{
498
    type Output = RasterTile2D<P>;
499
    type SpatialBounds = SpatialPartition2D;
500

501
    async fn _query<'a>(
4✔
502
        &'a self,
4✔
503
        query: RasterQueryRectangle,
4✔
504
        ctx: &'a dyn QueryContext,
4✔
505
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
4✔
506
        if let Some(state) = &self.state {
4✔
507
            let valid_bounds_in = state.valid_in_bounds;
4✔
508
            let valid_bounds_out = state.valid_out_bounds;
4✔
509

510
            // calculate the spatial resolution the input data should have using the intersection and the requested resolution
511
            let in_spatial_res = suggest_pixel_size_from_diag_cross_projected(
4✔
512
                valid_bounds_out,
4✔
513
                valid_bounds_in,
4✔
514
                query.spatial_resolution,
4✔
515
            )?;
4✔
516

517
            // setup the subquery
518
            let sub_query_spec = TileReprojectionSubQuery {
4✔
519
                in_srs: self.from,
4✔
520
                out_srs: self.to,
4✔
521
                fold_fn: fold_by_coordinate_lookup_future,
4✔
522
                in_spatial_res,
4✔
523
                valid_bounds_in,
4✔
524
                valid_bounds_out,
4✔
525
                _phantom_data: PhantomData,
4✔
526
            };
4✔
527

4✔
528
            // return the adapter which will reproject the tiles and uses the fill adapter to inject missing tiles
4✔
529
            Ok(RasterSubQueryAdapter::<'a, P, _, _>::new(
4✔
530
                &self.source,
4✔
531
                query,
4✔
532
                self.tiling_spec,
4✔
533
                ctx,
4✔
534
                sub_query_spec,
4✔
535
            )
4✔
536
            .filter_and_fill())
4✔
537
        } else {
538
            log::debug!("No intersection between source data / srs and target srs");
×
539

540
            let tiling_strat = self
×
541
                .tiling_spec
×
542
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
×
543

×
544
            let grid_bounds = tiling_strat.tile_grid_box(query.spatial_partition());
×
545
            Ok(Box::pin(SparseTilesFillAdapter::new(
×
546
                stream::empty(),
×
547
                grid_bounds,
×
548
                tiling_strat.geo_transform,
×
549
                self.tiling_spec.tile_size_in_pixels,
×
550
            )))
×
551
        }
552
    }
8✔
553
}
554

555
#[cfg(test)]
556
mod tests {
557
    use super::*;
558
    use crate::engine::{MockExecutionContext, MockQueryContext};
559
    use crate::mock::MockFeatureCollectionSource;
560
    use crate::mock::{MockRasterSource, MockRasterSourceParams};
561
    use crate::{
562
        engine::{ChunkByteSize, VectorOperator},
563
        source::{
564
            FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters,
565
            GdalMetaDataRegular, GdalMetaDataStatic, GdalSource, GdalSourceParameters,
566
            GdalSourceTimePlaceholder, TimeReference,
567
        },
568
        test_data,
569
        util::gdal::{add_ndvi_dataset, gdal_open_dataset},
570
    };
571
    use float_cmp::approx_eq;
572
    use futures::StreamExt;
573
    use geoengine_datatypes::collections::IntoGeometryIterator;
574
    use geoengine_datatypes::primitives::{
575
        AxisAlignedRectangle, Coordinate2D, DateTimeParseFormat,
576
    };
577
    use geoengine_datatypes::{
578
        collections::{
579
            GeometryCollection, MultiLineStringCollection, MultiPointCollection,
580
            MultiPolygonCollection,
581
        },
582
        dataset::{DataId, DatasetId},
583
        hashmap,
584
        primitives::{
585
            BoundingBox2D, Measurement, MultiLineString, MultiPoint, MultiPolygon, QueryRectangle,
586
            SpatialResolution, TimeGranularity, TimeInstance, TimeInterval, TimeStep,
587
        },
588
        raster::{Grid, GridShape, GridShape2D, GridSize, RasterDataType, RasterTile2D},
589
        spatial_reference::SpatialReferenceAuthority,
590
        util::{
591
            test::TestDefault,
592
            well_known_data::{
593
                COLOGNE_EPSG_4326, COLOGNE_EPSG_900_913, HAMBURG_EPSG_4326, HAMBURG_EPSG_900_913,
594
                MARBURG_EPSG_4326, MARBURG_EPSG_900_913,
595
            },
596
            Identifier,
597
        },
598
    };
599
    use std::collections::HashMap;
600
    use std::path::PathBuf;
601
    use std::str::FromStr;
602

603
    #[tokio::test]
1✔
604
    async fn multi_point() -> Result<()> {
1✔
605
        let points = MultiPointCollection::from_data(
1✔
606
            MultiPoint::many(vec![
1✔
607
                MARBURG_EPSG_4326,
1✔
608
                COLOGNE_EPSG_4326,
1✔
609
                HAMBURG_EPSG_4326,
1✔
610
            ])
1✔
611
            .unwrap(),
1✔
612
            vec![TimeInterval::new_unchecked(0, 1); 3],
1✔
613
            Default::default(),
1✔
614
        )?;
1✔
615

616
        let expected = MultiPoint::many(vec![
1✔
617
            MARBURG_EPSG_900_913,
1✔
618
            COLOGNE_EPSG_900_913,
1✔
619
            HAMBURG_EPSG_900_913,
1✔
620
        ])
1✔
621
        .unwrap();
1✔
622

1✔
623
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
624

1✔
625
        let target_spatial_reference =
1✔
626
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
627

628
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
629
            params: ReprojectionParams {
1✔
630
                target_spatial_reference,
1✔
631
            },
1✔
632
            sources: SingleRasterOrVectorSource {
1✔
633
                source: point_source.into(),
1✔
634
            },
1✔
635
        })
1✔
636
        .initialize(&MockExecutionContext::test_default())
1✔
637
        .await?;
×
638

639
        let query_processor = initialized_operator.query_processor()?;
1✔
640

641
        let query_processor = query_processor.multi_point().unwrap();
1✔
642

1✔
643
        let query_rectangle = VectorQueryRectangle {
1✔
644
            spatial_bounds: BoundingBox2D::new(
1✔
645
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
646
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
647
            )
1✔
648
            .unwrap(),
1✔
649
            time_interval: TimeInterval::default(),
1✔
650
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
651
        };
1✔
652
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
653

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

656
        let result = query
1✔
657
            .map(Result::unwrap)
1✔
658
            .collect::<Vec<MultiPointCollection>>()
1✔
659
            .await;
×
660

661
        assert_eq!(result.len(), 1);
1✔
662

663
        result[0]
1✔
664
            .geometries()
1✔
665
            .into_iter()
1✔
666
            .zip(expected.iter())
1✔
667
            .for_each(|(a, e)| {
3✔
668
                assert!(approx_eq!(&MultiPoint, &a.into(), e, epsilon = 0.00001));
3✔
669
            });
3✔
670

1✔
671
        Ok(())
1✔
672
    }
673

674
    #[tokio::test]
1✔
675
    async fn multi_lines() -> Result<()> {
1✔
676
        let lines = MultiLineStringCollection::from_data(
1✔
677
            vec![MultiLineString::new(vec![vec![
1✔
678
                MARBURG_EPSG_4326,
1✔
679
                COLOGNE_EPSG_4326,
1✔
680
                HAMBURG_EPSG_4326,
1✔
681
            ]])
1✔
682
            .unwrap()],
1✔
683
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
684
            Default::default(),
1✔
685
        )?;
1✔
686

687
        let expected = [MultiLineString::new(vec![vec![
1✔
688
            MARBURG_EPSG_900_913,
1✔
689
            COLOGNE_EPSG_900_913,
1✔
690
            HAMBURG_EPSG_900_913,
1✔
691
        ]])
1✔
692
        .unwrap()];
1✔
693

1✔
694
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
695

1✔
696
        let target_spatial_reference =
1✔
697
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
698

699
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
700
            params: ReprojectionParams {
1✔
701
                target_spatial_reference,
1✔
702
            },
1✔
703
            sources: SingleRasterOrVectorSource {
1✔
704
                source: lines_source.into(),
1✔
705
            },
1✔
706
        })
1✔
707
        .initialize(&MockExecutionContext::test_default())
1✔
708
        .await?;
×
709

710
        let query_processor = initialized_operator.query_processor()?;
1✔
711

712
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
713

1✔
714
        let query_rectangle = VectorQueryRectangle {
1✔
715
            spatial_bounds: BoundingBox2D::new(
1✔
716
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
717
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
718
            )
1✔
719
            .unwrap(),
1✔
720
            time_interval: TimeInterval::default(),
1✔
721
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
722
        };
1✔
723
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
724

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

727
        let result = query
1✔
728
            .map(Result::unwrap)
1✔
729
            .collect::<Vec<MultiLineStringCollection>>()
1✔
730
            .await;
×
731

732
        assert_eq!(result.len(), 1);
1✔
733

734
        result[0]
1✔
735
            .geometries()
1✔
736
            .into_iter()
1✔
737
            .zip(expected.iter())
1✔
738
            .for_each(|(a, e)| {
1✔
739
                assert!(approx_eq!(
1✔
740
                    &MultiLineString,
1✔
741
                    &a.into(),
1✔
742
                    e,
1✔
743
                    epsilon = 0.00001
1✔
744
                ));
1✔
745
            });
1✔
746

1✔
747
        Ok(())
1✔
748
    }
749

750
    #[tokio::test]
1✔
751
    async fn multi_polygons() -> Result<()> {
1✔
752
        let polygons = MultiPolygonCollection::from_data(
1✔
753
            vec![MultiPolygon::new(vec![vec![vec![
1✔
754
                MARBURG_EPSG_4326,
1✔
755
                COLOGNE_EPSG_4326,
1✔
756
                HAMBURG_EPSG_4326,
1✔
757
                MARBURG_EPSG_4326,
1✔
758
            ]]])
1✔
759
            .unwrap()],
1✔
760
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
761
            Default::default(),
1✔
762
        )?;
1✔
763

764
        let expected = [MultiPolygon::new(vec![vec![vec![
1✔
765
            MARBURG_EPSG_900_913,
1✔
766
            COLOGNE_EPSG_900_913,
1✔
767
            HAMBURG_EPSG_900_913,
1✔
768
            MARBURG_EPSG_900_913,
1✔
769
        ]]])
1✔
770
        .unwrap()];
1✔
771

1✔
772
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
773

1✔
774
        let target_spatial_reference =
1✔
775
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
776

777
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
778
            params: ReprojectionParams {
1✔
779
                target_spatial_reference,
1✔
780
            },
1✔
781
            sources: SingleRasterOrVectorSource {
1✔
782
                source: polygon_source.into(),
1✔
783
            },
1✔
784
        })
1✔
785
        .initialize(&MockExecutionContext::test_default())
1✔
786
        .await?;
×
787

788
        let query_processor = initialized_operator.query_processor()?;
1✔
789

790
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
791

1✔
792
        let query_rectangle = VectorQueryRectangle {
1✔
793
            spatial_bounds: BoundingBox2D::new(
1✔
794
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
795
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
796
            )
1✔
797
            .unwrap(),
1✔
798
            time_interval: TimeInterval::default(),
1✔
799
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
800
        };
1✔
801
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
802

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

805
        let result = query
1✔
806
            .map(Result::unwrap)
1✔
807
            .collect::<Vec<MultiPolygonCollection>>()
1✔
808
            .await;
×
809

810
        assert_eq!(result.len(), 1);
1✔
811

812
        result[0]
1✔
813
            .geometries()
1✔
814
            .into_iter()
1✔
815
            .zip(expected.iter())
1✔
816
            .for_each(|(a, e)| {
1✔
817
                assert!(approx_eq!(&MultiPolygon, &a.into(), e, epsilon = 0.00001));
1✔
818
            });
1✔
819

1✔
820
        Ok(())
1✔
821
    }
822

823
    #[tokio::test]
1✔
824
    async fn raster_identity() -> Result<()> {
1✔
825
        let projection = SpatialReference::new(
1✔
826
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
827
            4326,
1✔
828
        );
1✔
829

1✔
830
        let data = vec![
1✔
831
            RasterTile2D {
1✔
832
                time: TimeInterval::new_unchecked(0, 5),
1✔
833
                tile_position: [-1, 0].into(),
1✔
834
                global_geo_transform: TestDefault::test_default(),
1✔
835
                grid_array: Grid::new([2, 2].into(), vec![1, 2, 3, 4]).unwrap().into(),
1✔
836
                properties: Default::default(),
1✔
837
            },
1✔
838
            RasterTile2D {
1✔
839
                time: TimeInterval::new_unchecked(0, 5),
1✔
840
                tile_position: [-1, 1].into(),
1✔
841
                global_geo_transform: TestDefault::test_default(),
1✔
842
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
843
                properties: Default::default(),
1✔
844
            },
1✔
845
            RasterTile2D {
1✔
846
                time: TimeInterval::new_unchecked(5, 10),
1✔
847
                tile_position: [-1, 0].into(),
1✔
848
                global_geo_transform: TestDefault::test_default(),
1✔
849
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
850
                    .unwrap()
1✔
851
                    .into(),
1✔
852
                properties: Default::default(),
1✔
853
            },
1✔
854
            RasterTile2D {
1✔
855
                time: TimeInterval::new_unchecked(5, 10),
1✔
856
                tile_position: [-1, 1].into(),
1✔
857
                global_geo_transform: TestDefault::test_default(),
1✔
858
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
859
                    .unwrap()
1✔
860
                    .into(),
1✔
861
                properties: Default::default(),
1✔
862
            },
1✔
863
        ];
1✔
864

1✔
865
        let mrs1 = MockRasterSource {
1✔
866
            params: MockRasterSourceParams {
1✔
867
                data: data.clone(),
1✔
868
                result_descriptor: RasterResultDescriptor {
1✔
869
                    data_type: RasterDataType::U8,
1✔
870
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
871
                    measurement: Measurement::Unitless,
1✔
872
                    time: None,
1✔
873
                    bbox: None,
1✔
874
                    resolution: Some(SpatialResolution::one()),
1✔
875
                },
1✔
876
            },
1✔
877
        }
1✔
878
        .boxed();
1✔
879

1✔
880
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
881
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
1✔
882
            // we need a smaller tile size
1✔
883
            shape_array: [2, 2],
1✔
884
        };
1✔
885

1✔
886
        let query_ctx = MockQueryContext::test_default();
1✔
887

888
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
889
            params: ReprojectionParams {
1✔
890
                target_spatial_reference: projection, // This test will do a identity reprojection
1✔
891
            },
1✔
892
            sources: SingleRasterOrVectorSource {
1✔
893
                source: mrs1.into(),
1✔
894
            },
1✔
895
        })
1✔
896
        .initialize(&exe_ctx)
1✔
897
        .await?;
×
898

899
        let qp = initialized_operator
1✔
900
            .query_processor()
1✔
901
            .unwrap()
1✔
902
            .get_u8()
1✔
903
            .unwrap();
1✔
904

1✔
905
        let query_rect = RasterQueryRectangle {
1✔
906
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 1.).into(), (3., 0.).into()),
1✔
907
            time_interval: TimeInterval::new_unchecked(0, 10),
1✔
908
            spatial_resolution: SpatialResolution::one(),
1✔
909
        };
1✔
910

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

913
        let res = a
1✔
914
            .map(Result::unwrap)
1✔
915
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
916
            .await;
8✔
917
        assert_eq!(data, res);
1✔
918

919
        Ok(())
1✔
920
    }
921

922
    #[tokio::test]
1✔
923
    async fn raster_ndvi_3857() -> Result<()> {
1✔
924
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
925
        let query_ctx = MockQueryContext::test_default();
1✔
926
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
927
        exe_ctx.tiling_specification =
1✔
928
            TilingSpecification::new((0.0, 0.0).into(), [450, 450].into());
1✔
929

1✔
930
        let output_shape: GridShape2D = [900, 1800].into();
1✔
931
        let output_bounds =
1✔
932
            SpatialPartition2D::new_unchecked((0., 20_000_000.).into(), (20_000_000., 0.).into());
1✔
933
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001);
1✔
934
        // 2014-01-01
1✔
935

1✔
936
        let gdal_op = GdalSource {
1✔
937
            params: GdalSourceParameters { data: id.clone() },
1✔
938
        }
1✔
939
        .boxed();
1✔
940

1✔
941
        let projection = SpatialReference::new(
1✔
942
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
943
            3857,
1✔
944
        );
1✔
945

946
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
947
            params: ReprojectionParams {
1✔
948
                target_spatial_reference: projection,
1✔
949
            },
1✔
950
            sources: SingleRasterOrVectorSource {
1✔
951
                source: gdal_op.into(),
1✔
952
            },
1✔
953
        })
1✔
954
        .initialize(&exe_ctx)
1✔
955
        .await?;
×
956

957
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
958
        let y_query_resolution = output_bounds.size_y() / (output_shape.axis_size_y() * 2) as f64; // *2 to account for the dataset aspect ratio 2:1
1✔
959
        let spatial_resolution =
1✔
960
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
961

1✔
962
        let qp = initialized_operator
1✔
963
            .query_processor()
1✔
964
            .unwrap()
1✔
965
            .get_u8()
1✔
966
            .unwrap();
1✔
967

968
        let qs = qp
1✔
969
            .raster_query(
1✔
970
                RasterQueryRectangle {
1✔
971
                    spatial_bounds: output_bounds,
1✔
972
                    time_interval,
1✔
973
                    spatial_resolution,
1✔
974
                },
1✔
975
                &query_ctx,
1✔
976
            )
1✔
977
            .await
×
978
            .unwrap();
1✔
979

980
        let res = qs
1✔
981
            .map(Result::unwrap)
1✔
982
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
983
            .await;
124✔
984

985
        // Write the tiles to a file
986
        // let mut buffer = File::create("MOD13A2_M_NDVI_2014-04-01_tile-20.rst")?;
987
        // buffer.write(res[8].clone().into_materialized_tile().grid_array.data.as_slice())?;
988

989
        // This check is against a tile produced by the operator itself. It was visually validated. TODO: rebuild when open issues are solved.
990
        // A perfect validation would be against a GDAL output generated like this:
991
        // gdalwarp -t_srs EPSG:3857 -tr 11111.11111111 11111.11111111 -r near -te 0.0 5011111.111111112 5000000.0 10011111.111111112 -te_srs EPSG:3857 -of GTiff ./MOD13A2_M_NDVI_2014-04-01.TIFF ./MOD13A2_M_NDVI_2014-04-01_tile-20.rst
992

993
        assert_eq!(
1✔
994
            include_bytes!(
1✔
995
                "../../../test_data/raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01_tile-20.rst"
1✔
996
            ) as &[u8],
1✔
997
            res[8].clone().into_materialized_tile().grid_array.inner_grid.data.as_slice()
1✔
998
        );
1✔
999

1000
        Ok(())
1✔
1001
    }
1002

1003
    #[test]
1✔
1004
    fn query_rewrite_4326_3857() {
1✔
1005
        let query = VectorQueryRectangle {
1✔
1006
            spatial_bounds: BoundingBox2D::new_unchecked((-180., -90.).into(), (180., 90.).into()),
1✔
1007
            time_interval: TimeInterval::default(),
1✔
1008
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1009
        };
1✔
1010

1✔
1011
        let expected = BoundingBox2D::new_unchecked(
1✔
1012
            (-20_037_508.342_789_244, -20_048_966.104_014_594).into(),
1✔
1013
            (20_037_508.342_789_244, 20_048_966.104_014_594).into(),
1✔
1014
        );
1✔
1015

1✔
1016
        let reprojected = reproject_query(
1✔
1017
            query,
1✔
1018
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857),
1✔
1019
            SpatialReference::epsg_4326(),
1✔
1020
        )
1✔
1021
        .unwrap()
1✔
1022
        .unwrap();
1✔
1023

1✔
1024
        assert!(approx_eq!(
1✔
1025
            BoundingBox2D,
1✔
1026
            expected,
1✔
1027
            reprojected.spatial_bounds,
1✔
1028
            epsilon = 0.000_001
1✔
1029
        ));
1✔
1030
    }
1✔
1031

1032
    #[tokio::test]
1✔
1033
    async fn raster_ndvi_3857_to_4326() -> Result<()> {
1✔
1034
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1035
        let query_ctx = MockQueryContext::test_default();
1✔
1036

1✔
1037
        let m = GdalMetaDataRegular {
1✔
1038
            data_time: TimeInterval::new_unchecked(
1✔
1039
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1040
                TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1041
            ),
1✔
1042
            step: TimeStep {
1✔
1043
                granularity: TimeGranularity::Months,
1✔
1044
                step: 1,
1✔
1045
            },
1✔
1046
            time_placeholders: hashmap! {
1✔
1047
                "%_START_TIME_%".to_string() => GdalSourceTimePlaceholder {
1✔
1048
                    format: DateTimeParseFormat::custom("%Y-%m-%d".to_string()),
1✔
1049
                    reference: TimeReference::Start,
1✔
1050
                },
1✔
1051
            },
1✔
1052
            params: GdalDatasetParameters {
1✔
1053
                file_path: test_data!(
1✔
1054
                    "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_%_START_TIME_%.TIFF"
1✔
1055
                )
1✔
1056
                .into(),
1✔
1057
                rasterband_channel: 1,
1✔
1058
                geo_transform: GdalDatasetGeoTransform {
1✔
1059
                    origin_coordinate: (-20_037_508.342_789_244, 19_971_868.880_408_563).into(),
1✔
1060
                    x_pixel_size: 14_052.950_258_048_739,
1✔
1061
                    y_pixel_size: -14_057.881_117_788_405,
1✔
1062
                },
1✔
1063
                width: 2851,
1✔
1064
                height: 2841,
1✔
1065
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1066
                no_data_value: Some(0.),
1✔
1067
                properties_mapping: None,
1✔
1068
                gdal_open_options: None,
1✔
1069
                gdal_config_options: None,
1✔
1070
                allow_alphaband_as_mask: true,
1✔
1071
            },
1✔
1072
            result_descriptor: RasterResultDescriptor {
1✔
1073
                data_type: RasterDataType::U8,
1✔
1074
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857)
1✔
1075
                    .into(),
1✔
1076
                measurement: Measurement::Unitless,
1✔
1077
                time: None,
1✔
1078
                bbox: None,
1✔
1079
                resolution: None,
1✔
1080
            },
1✔
1081
        };
1✔
1082

1✔
1083
        let id: DataId = DatasetId::new().into();
1✔
1084
        exe_ctx.add_meta_data(id.clone(), Box::new(m));
1✔
1085

1✔
1086
        exe_ctx.tiling_specification = TilingSpecification::new((0.0, 0.0).into(), [60, 60].into());
1✔
1087

1✔
1088
        let output_bounds =
1✔
1089
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1090
        let time_interval = TimeInterval::new_unchecked(1_396_310_400_000, 1_396_310_400_000);
1✔
1091
        // 2014-04-01
1✔
1092

1✔
1093
        let gdal_op = GdalSource {
1✔
1094
            params: GdalSourceParameters { data: id.clone() },
1✔
1095
        }
1✔
1096
        .boxed();
1✔
1097

1098
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1099
            params: ReprojectionParams {
1✔
1100
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1101
            },
1✔
1102
            sources: SingleRasterOrVectorSource {
1✔
1103
                source: gdal_op.into(),
1✔
1104
            },
1✔
1105
        })
1✔
1106
        .initialize(&exe_ctx)
1✔
1107
        .await?;
×
1108

1109
        let x_query_resolution = output_bounds.size_x() / 480.; // since we request x -180 to 180 and y -90 to 90 with 60x60 tiles this will result in 8 x 4 tiles
1✔
1110
        let y_query_resolution = output_bounds.size_y() / 240.; // *2 to account for the dataset aspect ratio 2:1
1✔
1111
        let spatial_resolution =
1✔
1112
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1113

1✔
1114
        let qp = initialized_operator
1✔
1115
            .query_processor()
1✔
1116
            .unwrap()
1✔
1117
            .get_u8()
1✔
1118
            .unwrap();
1✔
1119

1120
        let qs = qp
1✔
1121
            .raster_query(
1✔
1122
                QueryRectangle {
1✔
1123
                    spatial_bounds: output_bounds,
1✔
1124
                    time_interval,
1✔
1125
                    spatial_resolution,
1✔
1126
                },
1✔
1127
                &query_ctx,
1✔
1128
            )
1✔
1129
            .await
×
1130
            .unwrap();
1✔
1131

1132
        let tiles = qs
1✔
1133
            .map(Result::unwrap)
1✔
1134
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1135
            .await;
233✔
1136

1137
        // the test must generate 8x4 tiles
1138
        assert_eq!(tiles.len(), 32);
1✔
1139

1140
        // none of the tiles should be empty
1141
        assert!(tiles.iter().all(|t| !t.is_empty()));
32✔
1142

1143
        Ok(())
1✔
1144
    }
1145

1146
    #[test]
1✔
1147
    fn source_resolution() {
1✔
1148
        let epsg_4326 = SpatialReference::epsg_4326();
1✔
1149
        let epsg_3857 = SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857);
1✔
1150

1✔
1151
        // use ndvi dataset that was reprojected using gdal as ground truth
1✔
1152
        let dataset_4326 = gdal_open_dataset(test_data!(
1✔
1153
            "raster/modis_ndvi/MOD13A2_M_NDVI_2014-04-01.TIFF"
1✔
1154
        ))
1✔
1155
        .unwrap();
1✔
1156
        let geotransform_4326 = dataset_4326.geo_transform().unwrap();
1✔
1157
        let res_4326 = SpatialResolution::new(geotransform_4326[1], -geotransform_4326[5]).unwrap();
1✔
1158

1✔
1159
        let dataset_3857 = gdal_open_dataset(test_data!(
1✔
1160
            "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01.TIFF"
1✔
1161
        ))
1✔
1162
        .unwrap();
1✔
1163
        let geotransform_3857 = dataset_3857.geo_transform().unwrap();
1✔
1164
        let res_3857 = SpatialResolution::new(geotransform_3857[1], -geotransform_3857[5]).unwrap();
1✔
1165

1✔
1166
        // ndvi was projected from 4326 to 3857. The calculated source_resolution for getting the raster in 3857 with `res_3857`
1✔
1167
        // should thus roughly be like the original `res_4326`
1✔
1168
        let result_res = suggest_pixel_size_from_diag_cross_projected::<SpatialPartition2D>(
1✔
1169
            epsg_3857.area_of_use_projected().unwrap(),
1✔
1170
            epsg_4326.area_of_use_projected().unwrap(),
1✔
1171
            res_3857,
1✔
1172
        )
1✔
1173
        .unwrap();
1✔
1174
        assert!(1. - (result_res.x / res_4326.x).abs() < 0.02);
1✔
1175
        assert!(1. - (result_res.y / res_4326.y).abs() < 0.02);
1✔
1176
    }
1✔
1177

1178
    #[tokio::test]
1✔
1179
    async fn query_outside_projection_area_of_use_produces_empty_tiles() {
1✔
1180
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1181
        let query_ctx = MockQueryContext::test_default();
1✔
1182

1✔
1183
        let m = GdalMetaDataStatic {
1✔
1184
            time: Some(TimeInterval::default()),
1✔
1185
            params: GdalDatasetParameters {
1✔
1186
                file_path: PathBuf::new(),
1✔
1187
                rasterband_channel: 1,
1✔
1188
                geo_transform: GdalDatasetGeoTransform {
1✔
1189
                    origin_coordinate: (166_021.44, 9_329_005.188).into(),
1✔
1190
                    x_pixel_size: (534_994.66 - 166_021.444) / 100.,
1✔
1191
                    y_pixel_size: -9_329_005.18 / 100.,
1✔
1192
                },
1✔
1193
                width: 100,
1✔
1194
                height: 100,
1✔
1195
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1196
                no_data_value: Some(0.),
1✔
1197
                properties_mapping: None,
1✔
1198
                gdal_open_options: None,
1✔
1199
                gdal_config_options: None,
1✔
1200
                allow_alphaband_as_mask: true,
1✔
1201
            },
1✔
1202
            result_descriptor: RasterResultDescriptor {
1✔
1203
                data_type: RasterDataType::U8,
1✔
1204
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636)
1✔
1205
                    .into(),
1✔
1206
                measurement: Measurement::Unitless,
1✔
1207
                time: None,
1✔
1208
                bbox: None,
1✔
1209
                resolution: None,
1✔
1210
            },
1✔
1211
        };
1✔
1212

1✔
1213
        let id: DataId = DatasetId::new().into();
1✔
1214
        exe_ctx.add_meta_data(id.clone(), Box::new(m));
1✔
1215

1✔
1216
        exe_ctx.tiling_specification =
1✔
1217
            TilingSpecification::new((0.0, 0.0).into(), [600, 600].into());
1✔
1218

1✔
1219
        let output_shape: GridShape2D = [1000, 1000].into();
1✔
1220
        let output_bounds =
1✔
1221
            SpatialPartition2D::new_unchecked((-180., 0.).into(), (180., -90.).into());
1✔
1222
        let time_interval = TimeInterval::new_instant(1_388_534_400_000).unwrap(); // 2014-01-01
1✔
1223

1✔
1224
        let gdal_op = GdalSource {
1✔
1225
            params: GdalSourceParameters { data: id.clone() },
1✔
1226
        }
1✔
1227
        .boxed();
1✔
1228

1229
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1230
            params: ReprojectionParams {
1✔
1231
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1232
            },
1✔
1233
            sources: SingleRasterOrVectorSource {
1✔
1234
                source: gdal_op.into(),
1✔
1235
            },
1✔
1236
        })
1✔
1237
        .initialize(&exe_ctx)
1✔
1238
        .await
×
1239
        .unwrap();
1✔
1240

1✔
1241
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
1242
        let y_query_resolution = output_bounds.size_y() / (output_shape.axis_size_y()) as f64;
1✔
1243
        let spatial_resolution =
1✔
1244
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1245

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

1252
        let result = qp
1✔
1253
            .raster_query(
1✔
1254
                QueryRectangle {
1✔
1255
                    spatial_bounds: output_bounds,
1✔
1256
                    time_interval,
1✔
1257
                    spatial_resolution,
1✔
1258
                },
1✔
1259
                &query_ctx,
1✔
1260
            )
1✔
1261
            .await
×
1262
            .unwrap()
1✔
1263
            .map(Result::unwrap)
1✔
1264
            .collect::<Vec<_>>()
1✔
1265
            .await;
×
1266

1267
        assert_eq!(result.len(), 4);
1✔
1268

1269
        for r in result {
5✔
1270
            assert!(r.is_empty());
4✔
1271
        }
1272
    }
1273

1274
    #[tokio::test]
1✔
1275
    async fn points_from_wgs84_to_utm36n() {
1✔
1276
        let exe_ctx = MockExecutionContext::test_default();
1✔
1277
        let query_ctx = MockQueryContext::test_default();
1✔
1278

1✔
1279
        let point_source = MockFeatureCollectionSource::single(
1✔
1280
            MultiPointCollection::from_data(
1✔
1281
                MultiPoint::many(vec![
1✔
1282
                    vec![(30.0, 0.0)], // lower left of utm36n area of use
1✔
1283
                    vec![(36.0, 84.0)],
1✔
1284
                    vec![(33.0, 42.0)], // upper right of utm36n area of use
1✔
1285
                ])
1✔
1286
                .unwrap(),
1✔
1287
                vec![TimeInterval::default(); 3],
1✔
1288
                HashMap::default(),
1✔
1289
            )
1✔
1290
            .unwrap(),
1✔
1291
        )
1✔
1292
        .boxed();
1✔
1293

1294
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1295
            params: ReprojectionParams {
1✔
1296
                target_spatial_reference: SpatialReference::new(
1✔
1297
                    SpatialReferenceAuthority::Epsg,
1✔
1298
                    32636, // utm36n
1✔
1299
                ),
1✔
1300
            },
1✔
1301
            sources: SingleRasterOrVectorSource {
1✔
1302
                source: point_source.into(),
1✔
1303
            },
1✔
1304
        })
1✔
1305
        .initialize(&exe_ctx)
1✔
1306
        .await
×
1307
        .unwrap();
1✔
1308

1✔
1309
        let qp = initialized_operator
1✔
1310
            .query_processor()
1✔
1311
            .unwrap()
1✔
1312
            .multi_point()
1✔
1313
            .unwrap();
1✔
1314

1✔
1315
        let spatial_bounds = BoundingBox2D::new(
1✔
1316
            (166_021.44, 0.00).into(), // lower left of projected utm36n area of use
1✔
1317
            (534_994.666_6, 9_329_005.18).into(), // upper right of projected utm36n area of use
1✔
1318
        )
1✔
1319
        .unwrap();
1✔
1320

1321
        let qs = qp
1✔
1322
            .vector_query(
1✔
1323
                QueryRectangle {
1✔
1324
                    spatial_bounds,
1✔
1325
                    time_interval: TimeInterval::default(),
1✔
1326
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1327
                },
1✔
1328
                &query_ctx,
1✔
1329
            )
1✔
1330
            .await
×
1331
            .unwrap();
1✔
1332

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

1335
        assert_eq!(points.len(), 1);
1✔
1336

1337
        let points = &points[0];
1✔
1338

1✔
1339
        assert_eq!(
1✔
1340
            points.coordinates(),
1✔
1341
            &[
1✔
1342
                (166_021.443_080_538_42, 0.0).into(),
1✔
1343
                (534_994.655_061_136_1, 9_329_005.182_447_437).into(),
1✔
1344
                (499_999.999_999_999_5, 4_649_776.224_819_178).into()
1✔
1345
            ]
1✔
1346
        );
1✔
1347
    }
1348

1349
    #[tokio::test]
1✔
1350
    async fn points_from_utm36n_to_wgs84() {
1✔
1351
        let exe_ctx = MockExecutionContext::test_default();
1✔
1352
        let query_ctx = MockQueryContext::test_default();
1✔
1353

1✔
1354
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1355
            vec![MultiPointCollection::from_data(
1✔
1356
                MultiPoint::many(vec![
1✔
1357
                    vec![(166_021.443_080_538_42, 0.0)],
1✔
1358
                    vec![(534_994.655_061_136_1, 9_329_005.182_447_437)],
1✔
1359
                    vec![(499_999.999_999_999_5, 4_649_776.224_819_178)],
1✔
1360
                ])
1✔
1361
                .unwrap(),
1✔
1362
                vec![TimeInterval::default(); 3],
1✔
1363
                HashMap::default(),
1✔
1364
            )
1✔
1365
            .unwrap()],
1✔
1366
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1367
        )
1✔
1368
        .boxed();
1✔
1369

1370
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1371
            params: ReprojectionParams {
1✔
1372
                target_spatial_reference: SpatialReference::new(
1✔
1373
                    SpatialReferenceAuthority::Epsg,
1✔
1374
                    4326, // utm36n
1✔
1375
                ),
1✔
1376
            },
1✔
1377
            sources: SingleRasterOrVectorSource {
1✔
1378
                source: point_source.into(),
1✔
1379
            },
1✔
1380
        })
1✔
1381
        .initialize(&exe_ctx)
1✔
1382
        .await
×
1383
        .unwrap();
1✔
1384

1✔
1385
        let qp = initialized_operator
1✔
1386
            .query_processor()
1✔
1387
            .unwrap()
1✔
1388
            .multi_point()
1✔
1389
            .unwrap();
1✔
1390

1✔
1391
        let spatial_bounds = BoundingBox2D::new(
1✔
1392
            (30.0, 0.0).into(),  // lower left of utm36n area of use
1✔
1393
            (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1394
        )
1✔
1395
        .unwrap();
1✔
1396

1397
        let qs = qp
1✔
1398
            .vector_query(
1✔
1399
                QueryRectangle {
1✔
1400
                    spatial_bounds,
1✔
1401
                    time_interval: TimeInterval::default(),
1✔
1402
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1403
                },
1✔
1404
                &query_ctx,
1✔
1405
            )
1✔
1406
            .await
×
1407
            .unwrap();
1✔
1408

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

1411
        assert_eq!(points.len(), 1);
1✔
1412

1413
        let points = &points[0];
1✔
1414

1✔
1415
        assert!(approx_eq!(
1✔
1416
            &[Coordinate2D],
1✔
1417
            points.coordinates(),
1✔
1418
            &[
1✔
1419
                (30.0, 0.0).into(), // lower left of utm36n area of use
1✔
1420
                (36.0, 84.0).into(),
1✔
1421
                (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1422
            ]
1✔
1423
        ));
1✔
1424
    }
1425

1426
    #[tokio::test]
1✔
1427
    async fn points_from_utm36n_to_wgs84_out_of_area() {
1✔
1428
        // 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
1✔
1429

1✔
1430
        let exe_ctx = MockExecutionContext::test_default();
1✔
1431
        let query_ctx = MockQueryContext::test_default();
1✔
1432

1✔
1433
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1434
            vec![MultiPointCollection::from_data(
1✔
1435
                MultiPoint::many(vec![
1✔
1436
                    vec![(758_565., 4_928_353.)], // (12.25, 44,46)
1✔
1437
                ])
1✔
1438
                .unwrap(),
1✔
1439
                vec![TimeInterval::default(); 1],
1✔
1440
                HashMap::default(),
1✔
1441
            )
1✔
1442
            .unwrap()],
1✔
1443
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1444
        )
1✔
1445
        .boxed();
1✔
1446

1447
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1448
            params: ReprojectionParams {
1✔
1449
                target_spatial_reference: SpatialReference::new(
1✔
1450
                    SpatialReferenceAuthority::Epsg,
1✔
1451
                    4326, // utm36n
1✔
1452
                ),
1✔
1453
            },
1✔
1454
            sources: SingleRasterOrVectorSource {
1✔
1455
                source: point_source.into(),
1✔
1456
            },
1✔
1457
        })
1✔
1458
        .initialize(&exe_ctx)
1✔
1459
        .await
×
1460
        .unwrap();
1✔
1461

1✔
1462
        let qp = initialized_operator
1✔
1463
            .query_processor()
1✔
1464
            .unwrap()
1✔
1465
            .multi_point()
1✔
1466
            .unwrap();
1✔
1467

1✔
1468
        let spatial_bounds = BoundingBox2D::new(
1✔
1469
            (10.0, 0.0).into(),  // -20 x values left of lower left of utm36n area of use
1✔
1470
            (13.0, 42.0).into(), // -20 x values left of upper right of utm36n area of use
1✔
1471
        )
1✔
1472
        .unwrap();
1✔
1473

1474
        let qs = qp
1✔
1475
            .vector_query(
1✔
1476
                QueryRectangle {
1✔
1477
                    spatial_bounds,
1✔
1478
                    time_interval: TimeInterval::default(),
1✔
1479
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1480
                },
1✔
1481
                &query_ctx,
1✔
1482
            )
1✔
1483
            .await
×
1484
            .unwrap();
1✔
1485

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

1488
        assert_eq!(points.len(), 1);
1✔
1489

1490
        let points = &points[0];
1✔
1491

1✔
1492
        assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1✔
1493
        assert!(points.coordinates().is_empty());
1✔
1494
    }
1495

1496
    #[test]
1✔
1497
    fn it_derives_raster_result_descriptor() {
1✔
1498
        let in_proj = SpatialReference::epsg_4326();
1✔
1499
        let out_proj = SpatialReference::from_str("EPSG:3857").unwrap();
1✔
1500
        let bbox = Some(SpatialPartition2D::new_unchecked(
1✔
1501
            (-180., 90.).into(),
1✔
1502
            (180., -90.).into(),
1✔
1503
        ));
1✔
1504

1✔
1505
        let resolution = Some(SpatialResolution::new_unchecked(0.1, 0.1));
1✔
1506

1✔
1507
        let (in_bounds, out_bounds, out_res) =
1✔
1508
            InitializedRasterReprojection::derive_raster_in_bounds_out_bounds_out_res(
1✔
1509
                in_proj, out_proj, resolution, bbox,
1✔
1510
            )
1✔
1511
            .unwrap();
1✔
1512

1✔
1513
        assert_eq!(
1✔
1514
            in_bounds.unwrap(),
1✔
1515
            SpatialPartition2D::new_unchecked((-180., 85.06).into(), (180., -85.06).into(),)
1✔
1516
        );
1✔
1517

1518
        assert_eq!(
1✔
1519
            out_bounds.unwrap(),
1✔
1520
            out_proj
1✔
1521
                .area_of_use_projected::<SpatialPartition2D>()
1✔
1522
                .unwrap()
1✔
1523
        );
1✔
1524

1525
        // TODO: y resolution should be double the x resolution, but currently we only compute a uniform resolution
1526
        assert_eq!(
1✔
1527
            out_res.unwrap(),
1✔
1528
            SpatialResolution::new_unchecked(14_237.781_884_528_267, 14_237.781_884_528_267),
1✔
1529
        );
1✔
1530
    }
1✔
1531
}
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

© 2025 Coveralls, Inc