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

geo-engine / geoengine / 7276004606

20 Dec 2023 01:21PM UTC coverage: 89.793% (-0.03%) from 89.823%
7276004606

push

github

web-flow
Merge pull request #906 from geo-engine/raster_result_describer

result descriptors for query processors

1080 of 1240 new or added lines in 43 files covered. (87.1%)

14 existing lines in 6 files now uncovered.

115914 of 129090 relevant lines covered (89.79%)

58688.76 hits per line

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

88.16
/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, FillerTileCacheExpirationStrategy, RasterSubQueryAdapter,
7
        SparseTilesFillAdapter, TileReprojectionSubQuery,
8
    },
9
    engine::{
10
        CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources,
11
        InitializedVectorOperator, Operator, OperatorName, QueryContext, QueryProcessor,
12
        RasterOperator, RasterQueryProcessor, RasterResultDescriptor, SingleRasterOrVectorSource,
13
        TypedRasterQueryProcessor, TypedVectorQueryProcessor, VectorOperator, VectorQueryProcessor,
14
        VectorResultDescriptor, WorkflowOperatorPath,
15
    },
16
    error::{self, Error},
17
    util::Result,
18
};
19
use async_trait::async_trait;
20
use futures::stream::BoxStream;
21
use futures::{stream, StreamExt};
22
use geoengine_datatypes::{
23
    collections::FeatureCollection,
24
    operations::reproject::{
25
        reproject_and_unify_bbox, reproject_query, suggest_pixel_size_from_diag_cross_projected,
26
        CoordinateProjection, CoordinateProjector, Reproject, ReprojectClipped,
27
    },
28
    primitives::{
29
        BandSelection, BoundingBox2D, ColumnSelection, Geometry, RasterQueryRectangle,
30
        SpatialPartition2D, SpatialPartitioned, SpatialResolution, VectorQueryRectangle,
31
    },
32
    raster::{Pixel, RasterTile2D, TilingSpecification},
33
    spatial_reference::SpatialReference,
34
    util::arrow::ArrowTyped,
35
};
36
use serde::{Deserialize, Serialize};
37
use snafu::ensure;
38

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

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

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

53
impl Reprojection {}
54

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

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

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

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

91
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
92
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
93

94
        let bbox = if let Some(bbox) = in_desc.bbox {
6✔
95
            let projector =
×
96
                CoordinateProjector::from_known_srs(in_srs, params.target_spatial_reference)?;
×
97

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

103
        let out_desc = VectorResultDescriptor {
6✔
104
            spatial_reference: params.target_spatial_reference.into(),
6✔
105
            data_type: in_desc.data_type,
6✔
106
            columns: in_desc.columns.clone(),
6✔
107
            time: in_desc.time,
6✔
108
            bbox,
6✔
109
        };
6✔
110

6✔
111
        Ok(InitializedVectorReprojection {
6✔
112
            name,
6✔
113
            result_descriptor: out_desc,
6✔
114
            source: source_vector_operator,
6✔
115
            source_srs: in_srs,
6✔
116
            target_srs: params.target_spatial_reference,
6✔
117
        })
6✔
118
    }
6✔
119
}
120

121
impl InitializedRasterReprojection {
122
    pub fn try_new_with_input(
6✔
123
        name: CanonicOperatorName,
6✔
124
        params: ReprojectionParams,
6✔
125
        source_raster_operator: Box<dyn InitializedRasterOperator>,
6✔
126
        tiling_spec: TilingSpecification,
6✔
127
    ) -> Result<Self> {
6✔
128
        let in_desc: RasterResultDescriptor = source_raster_operator.result_descriptor().clone();
6✔
129

130
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
131
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
132

133
        // calculate the intersection of input and output srs in both coordinate systems
134
        let (in_bounds, out_bounds, out_res) = Self::derive_raster_in_bounds_out_bounds_out_res(
6✔
135
            in_srs,
6✔
136
            params.target_spatial_reference,
6✔
137
            in_desc.resolution,
6✔
138
            in_desc.bbox,
6✔
139
        )?;
6✔
140

141
        let result_descriptor = RasterResultDescriptor {
4✔
142
            spatial_reference: params.target_spatial_reference.into(),
4✔
143
            data_type: in_desc.data_type,
4✔
144
            time: in_desc.time,
4✔
145
            bbox: out_bounds,
4✔
146
            resolution: out_res,
4✔
147
            bands: in_desc.bands.clone(),
4✔
148
        };
4✔
149

150
        let state = match (in_bounds, out_bounds) {
4✔
151
            (Some(in_bounds), Some(out_bounds)) => Some(ReprojectionBounds {
4✔
152
                valid_in_bounds: in_bounds,
4✔
153
                valid_out_bounds: out_bounds,
4✔
154
            }),
4✔
155
            _ => None,
×
156
        };
157

158
        Ok(InitializedRasterReprojection {
4✔
159
            name,
4✔
160
            result_descriptor,
4✔
161
            source: source_raster_operator,
4✔
162
            state,
4✔
163
            source_srs: in_srs,
4✔
164
            target_srs: params.target_spatial_reference,
4✔
165
            tiling_spec,
4✔
166
        })
4✔
167
    }
6✔
168

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

186
            (valid_bounds_in, valid_bounds_out)
3✔
187
        };
188

189
        let out_res = match (source_spatial_resolution, in_bbox, out_bbox) {
5✔
190
            (Some(in_res), Some(in_bbox), Some(out_bbox)) => {
3✔
191
                suggest_pixel_size_from_diag_cross_projected(in_bbox, out_bbox, in_res).ok()
3✔
192
            }
193
            _ => None,
2✔
194
        };
195

196
        Ok((in_bbox, out_bbox, out_res))
5✔
197
    }
7✔
198
}
199

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

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

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

220
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
6✔
221
            name,
6✔
222
            self.params,
6✔
223
            initialized_source.vector,
6✔
224
        )?;
6✔
225

226
        Ok(initialized_operator.boxed())
6✔
227
    }
14✔
228

229
    span_fn!(Reprojection);
×
230
}
231

232
impl InitializedVectorOperator for InitializedVectorReprojection {
233
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
234
        &self.result_descriptor
×
235
    }
×
236

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

286
    fn canonic_name(&self) -> CanonicOperatorName {
×
287
        self.name.clone()
×
288
    }
×
289
}
290

291
struct VectorReprojectionProcessor<Q, G>
292
where
293
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
294
{
295
    source: Q,
296
    result_descriptor: VectorResultDescriptor,
297
    from: SpatialReference,
298
    to: SpatialReference,
299
}
300

301
impl<Q, G> VectorReprojectionProcessor<Q, G>
302
where
303
    Q: VectorQueryProcessor<VectorType = FeatureCollection<G>>,
304
{
305
    pub fn new(
6✔
306
        source: Q,
6✔
307
        result_descriptor: VectorResultDescriptor,
6✔
308
        from: SpatialReference,
6✔
309
        to: SpatialReference,
6✔
310
    ) -> Self {
6✔
311
        Self {
6✔
312
            source,
6✔
313
            result_descriptor,
6✔
314
            from,
6✔
315
            to,
6✔
316
        }
6✔
317
    }
6✔
318
}
319

320
#[async_trait]
321
impl<Q, G> QueryProcessor for VectorReprojectionProcessor<Q, G>
322
where
323
    Q: QueryProcessor<
324
        Output = FeatureCollection<G>,
325
        SpatialBounds = BoundingBox2D,
326
        Selection = ColumnSelection,
327
        ResultDescription = VectorResultDescriptor,
328
    >,
329
    FeatureCollection<G>: Reproject<CoordinateProjector, Out = FeatureCollection<G>>,
330
    G: Geometry + ArrowTyped,
331
{
332
    type Output = FeatureCollection<G>;
333
    type SpatialBounds = BoundingBox2D;
334
    type Selection = ColumnSelection;
335
    type ResultDescription = VectorResultDescriptor;
336

337
    async fn _query<'a>(
6✔
338
        &'a self,
6✔
339
        query: VectorQueryRectangle,
6✔
340
        ctx: &'a dyn QueryContext,
6✔
341
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
6✔
342
        let rewritten_query = reproject_query(query, self.from, self.to)?;
6✔
343

344
        if let Some(rewritten_query) = rewritten_query {
6✔
345
            Ok(self
5✔
346
                .source
5✔
347
                .query(rewritten_query, ctx)
5✔
348
                .await?
×
349
                .map(move |collection_result| {
5✔
350
                    collection_result.and_then(|collection| {
5✔
351
                        CoordinateProjector::from_known_srs(self.from, self.to)
5✔
352
                            .and_then(|projector| collection.reproject(projector.as_ref()))
5✔
353
                            .map_err(Into::into)
5✔
354
                    })
5✔
355
                })
5✔
356
                .boxed())
5✔
357
        } else {
358
            let res = Ok(FeatureCollection::empty());
1✔
359
            Ok(Box::pin(stream::once(async { res })))
1✔
360
        }
361
    }
12✔
362

363
    fn result_descriptor(&self) -> &VectorResultDescriptor {
12✔
364
        &self.result_descriptor
12✔
365
    }
12✔
366
}
367

368
#[typetag::serde]
×
369
#[async_trait]
370
impl RasterOperator for Reprojection {
371
    async fn _initialize(
4✔
372
        self: Box<Self>,
4✔
373
        path: WorkflowOperatorPath,
4✔
374
        context: &dyn ExecutionContext,
4✔
375
    ) -> Result<Box<dyn InitializedRasterOperator>> {
4✔
376
        let name = CanonicOperatorName::from(&self);
4✔
377

378
        let raster_source =
4✔
379
            self.sources
4✔
380
                .raster()
4✔
381
                .ok_or_else(|| error::Error::InvalidOperatorType {
4✔
382
                    expected: "Raster".to_owned(),
×
383
                    found: "Vector".to_owned(),
×
384
                })?;
4✔
385

386
        let initialized_source = raster_source.initialize_sources(path, context).await?;
4✔
387

388
        // TODO: implement multi-band functionality and remove this check
389
        ensure!(
4✔
390
            initialized_source.raster.result_descriptor().bands.len() == 1,
4✔
391
            crate::error::OperatorDoesNotSupportMultiBandsSourcesYet {
×
392
                operator: Reprojection::TYPE_NAME
×
393
            }
×
394
        );
395

396
        let initialized_operator = InitializedRasterReprojection::try_new_with_input(
4✔
397
            name,
4✔
398
            self.params,
4✔
399
            initialized_source.raster,
4✔
400
            context.tiling_specification(),
4✔
401
        )?;
4✔
402

403
        Ok(initialized_operator.boxed())
4✔
404
    }
8✔
405

406
    span_fn!(Reprojection);
×
407
}
408

409
impl InitializedRasterOperator for InitializedRasterReprojection {
410
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
411
        &self.result_descriptor
×
412
    }
×
413

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

419
        Ok(match self.result_descriptor.data_type {
4✔
420
            geoengine_datatypes::raster::RasterDataType::U8 => {
421
                let qt = q.get_u8().unwrap();
4✔
422
                TypedRasterQueryProcessor::U8(Box::new(RasterReprojectionProcessor::new(
4✔
423
                    qt,
4✔
424
                    self.result_descriptor.clone(),
4✔
425
                    self.source_srs,
4✔
426
                    self.target_srs,
4✔
427
                    self.tiling_spec,
4✔
428
                    self.state,
4✔
429
                )))
4✔
430
            }
431
            geoengine_datatypes::raster::RasterDataType::U16 => {
432
                let qt = q.get_u16().unwrap();
×
433
                TypedRasterQueryProcessor::U16(Box::new(RasterReprojectionProcessor::new(
×
434
                    qt,
×
NEW
435
                    self.result_descriptor.clone(),
×
436
                    self.source_srs,
×
437
                    self.target_srs,
×
438
                    self.tiling_spec,
×
439
                    self.state,
×
440
                )))
×
441
            }
442

443
            geoengine_datatypes::raster::RasterDataType::U32 => {
444
                let qt = q.get_u32().unwrap();
×
445
                TypedRasterQueryProcessor::U32(Box::new(RasterReprojectionProcessor::new(
×
446
                    qt,
×
NEW
447
                    self.result_descriptor.clone(),
×
448
                    self.source_srs,
×
449
                    self.target_srs,
×
450
                    self.tiling_spec,
×
451
                    self.state,
×
452
                )))
×
453
            }
454
            geoengine_datatypes::raster::RasterDataType::U64 => {
455
                let qt = q.get_u64().unwrap();
×
456
                TypedRasterQueryProcessor::U64(Box::new(RasterReprojectionProcessor::new(
×
457
                    qt,
×
NEW
458
                    self.result_descriptor.clone(),
×
459
                    self.source_srs,
×
460
                    self.target_srs,
×
461
                    self.tiling_spec,
×
462
                    self.state,
×
463
                )))
×
464
            }
465
            geoengine_datatypes::raster::RasterDataType::I8 => {
466
                let qt = q.get_i8().unwrap();
×
467
                TypedRasterQueryProcessor::I8(Box::new(RasterReprojectionProcessor::new(
×
468
                    qt,
×
NEW
469
                    self.result_descriptor.clone(),
×
470
                    self.source_srs,
×
471
                    self.target_srs,
×
472
                    self.tiling_spec,
×
473
                    self.state,
×
474
                )))
×
475
            }
476
            geoengine_datatypes::raster::RasterDataType::I16 => {
477
                let qt = q.get_i16().unwrap();
×
478
                TypedRasterQueryProcessor::I16(Box::new(RasterReprojectionProcessor::new(
×
479
                    qt,
×
NEW
480
                    self.result_descriptor.clone(),
×
481
                    self.source_srs,
×
482
                    self.target_srs,
×
483
                    self.tiling_spec,
×
484
                    self.state,
×
485
                )))
×
486
            }
487
            geoengine_datatypes::raster::RasterDataType::I32 => {
488
                let qt = q.get_i32().unwrap();
×
489
                TypedRasterQueryProcessor::I32(Box::new(RasterReprojectionProcessor::new(
×
490
                    qt,
×
NEW
491
                    self.result_descriptor.clone(),
×
492
                    self.source_srs,
×
493
                    self.target_srs,
×
494
                    self.tiling_spec,
×
495
                    self.state,
×
496
                )))
×
497
            }
498
            geoengine_datatypes::raster::RasterDataType::I64 => {
499
                let qt = q.get_i64().unwrap();
×
500
                TypedRasterQueryProcessor::I64(Box::new(RasterReprojectionProcessor::new(
×
501
                    qt,
×
NEW
502
                    self.result_descriptor.clone(),
×
503
                    self.source_srs,
×
504
                    self.target_srs,
×
505
                    self.tiling_spec,
×
506
                    self.state,
×
507
                )))
×
508
            }
509
            geoengine_datatypes::raster::RasterDataType::F32 => {
510
                let qt = q.get_f32().unwrap();
×
511
                TypedRasterQueryProcessor::F32(Box::new(RasterReprojectionProcessor::new(
×
512
                    qt,
×
NEW
513
                    self.result_descriptor.clone(),
×
514
                    self.source_srs,
×
515
                    self.target_srs,
×
516
                    self.tiling_spec,
×
517
                    self.state,
×
518
                )))
×
519
            }
520
            geoengine_datatypes::raster::RasterDataType::F64 => {
521
                let qt = q.get_f64().unwrap();
×
522
                TypedRasterQueryProcessor::F64(Box::new(RasterReprojectionProcessor::new(
×
523
                    qt,
×
NEW
524
                    self.result_descriptor.clone(),
×
525
                    self.source_srs,
×
526
                    self.target_srs,
×
527
                    self.tiling_spec,
×
528
                    self.state,
×
529
                )))
×
530
            }
531
        })
532
    }
4✔
533

534
    fn canonic_name(&self) -> CanonicOperatorName {
×
535
        self.name.clone()
×
536
    }
×
537
}
538

539
pub struct RasterReprojectionProcessor<Q, P>
540
where
541
    Q: RasterQueryProcessor<RasterType = P>,
542
{
543
    source: Q,
544
    result_descriptor: RasterResultDescriptor,
545
    from: SpatialReference,
546
    to: SpatialReference,
547
    tiling_spec: TilingSpecification,
548
    state: Option<ReprojectionBounds>,
549
    _phantom_data: PhantomData<P>,
550
}
551

552
impl<Q, P> RasterReprojectionProcessor<Q, P>
553
where
554
    Q: RasterQueryProcessor<RasterType = P>,
555
    P: Pixel,
556
{
557
    pub fn new(
4✔
558
        source: Q,
4✔
559
        result_descriptor: RasterResultDescriptor,
4✔
560
        from: SpatialReference,
4✔
561
        to: SpatialReference,
4✔
562
        tiling_spec: TilingSpecification,
4✔
563
        state: Option<ReprojectionBounds>,
4✔
564
    ) -> Self {
4✔
565
        Self {
4✔
566
            source,
4✔
567
            result_descriptor,
4✔
568
            from,
4✔
569
            to,
4✔
570
            tiling_spec,
4✔
571
            state,
4✔
572
            _phantom_data: PhantomData,
4✔
573
        }
4✔
574
    }
4✔
575
}
576

577
#[async_trait]
578
impl<Q, P> QueryProcessor for RasterReprojectionProcessor<Q, P>
579
where
580
    Q: QueryProcessor<
581
        Output = RasterTile2D<P>,
582
        SpatialBounds = SpatialPartition2D,
583
        Selection = BandSelection,
584
        ResultDescription = RasterResultDescriptor,
585
    >,
586
    P: Pixel,
587
{
588
    type Output = RasterTile2D<P>;
589
    type SpatialBounds = SpatialPartition2D;
590
    type Selection = BandSelection;
591
    type ResultDescription = RasterResultDescriptor;
592

593
    async fn _query<'a>(
4✔
594
        &'a self,
4✔
595
        query: RasterQueryRectangle,
4✔
596
        ctx: &'a dyn QueryContext,
4✔
597
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
4✔
598
        if let Some(state) = &self.state {
4✔
599
            let valid_bounds_in = state.valid_in_bounds;
4✔
600
            let valid_bounds_out = state.valid_out_bounds;
4✔
601

602
            // calculate the spatial resolution the input data should have using the intersection and the requested resolution
603
            let in_spatial_res = suggest_pixel_size_from_diag_cross_projected(
4✔
604
                valid_bounds_out,
4✔
605
                valid_bounds_in,
4✔
606
                query.spatial_resolution,
4✔
607
            )?;
4✔
608

609
            // setup the subquery
610
            let sub_query_spec = TileReprojectionSubQuery {
4✔
611
                in_srs: self.from,
4✔
612
                out_srs: self.to,
4✔
613
                fold_fn: fold_by_coordinate_lookup_future,
4✔
614
                in_spatial_res,
4✔
615
                valid_bounds_in,
4✔
616
                valid_bounds_out,
4✔
617
                _phantom_data: PhantomData,
4✔
618
            };
4✔
619

4✔
620
            // return the adapter which will reproject the tiles and uses the fill adapter to inject missing tiles
4✔
621
            Ok(RasterSubQueryAdapter::<'a, P, _, _>::new(
4✔
622
                &self.source,
4✔
623
                query,
4✔
624
                self.tiling_spec,
4✔
625
                ctx,
4✔
626
                sub_query_spec,
4✔
627
            )
4✔
628
            .filter_and_fill(FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles))
4✔
629
        } else {
630
            log::debug!("No intersection between source data / srs and target srs");
×
631

632
            let tiling_strat = self
×
633
                .tiling_spec
×
634
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
×
635

×
636
            let grid_bounds = tiling_strat.tile_grid_box(query.spatial_partition());
×
637
            Ok(Box::pin(SparseTilesFillAdapter::new(
×
638
                stream::empty(),
×
639
                grid_bounds,
×
640
                query.attributes.count(),
×
641
                tiling_strat.geo_transform,
×
642
                self.tiling_spec.tile_size_in_pixels,
×
643
                FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles,
×
644
            )))
×
645
        }
646
    }
8✔
647

648
    fn result_descriptor(&self) -> &RasterResultDescriptor {
8✔
649
        &self.result_descriptor
8✔
650
    }
8✔
651
}
652

653
#[cfg(test)]
654
mod tests {
655
    use super::*;
656
    use crate::engine::{MockExecutionContext, MockQueryContext, RasterBandDescriptors};
657
    use crate::mock::MockFeatureCollectionSource;
658
    use crate::mock::{MockRasterSource, MockRasterSourceParams};
659
    use crate::{
660
        engine::{ChunkByteSize, VectorOperator},
661
        source::{
662
            FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters,
663
            GdalMetaDataRegular, GdalMetaDataStatic, GdalSource, GdalSourceParameters,
664
            GdalSourceTimePlaceholder, TimeReference,
665
        },
666
        test_data,
667
        util::gdal::{add_ndvi_dataset, gdal_open_dataset},
668
    };
669
    use float_cmp::approx_eq;
670
    use futures::StreamExt;
671
    use geoengine_datatypes::collections::IntoGeometryIterator;
672
    use geoengine_datatypes::dataset::NamedData;
673
    use geoengine_datatypes::primitives::{
674
        AxisAlignedRectangle, Coordinate2D, DateTimeParseFormat,
675
    };
676
    use geoengine_datatypes::primitives::{CacheHint, CacheTtlSeconds};
677
    use geoengine_datatypes::raster::TilesEqualIgnoringCacheHint;
678
    use geoengine_datatypes::{
679
        collections::{
680
            GeometryCollection, MultiLineStringCollection, MultiPointCollection,
681
            MultiPolygonCollection,
682
        },
683
        dataset::{DataId, DatasetId},
684
        hashmap,
685
        primitives::{
686
            BoundingBox2D, MultiLineString, MultiPoint, MultiPolygon, QueryRectangle,
687
            SpatialResolution, TimeGranularity, TimeInstance, TimeInterval, TimeStep,
688
        },
689
        raster::{Grid, GridShape, GridShape2D, GridSize, RasterDataType, RasterTile2D},
690
        spatial_reference::SpatialReferenceAuthority,
691
        util::{
692
            test::TestDefault,
693
            well_known_data::{
694
                COLOGNE_EPSG_4326, COLOGNE_EPSG_900_913, HAMBURG_EPSG_4326, HAMBURG_EPSG_900_913,
695
                MARBURG_EPSG_4326, MARBURG_EPSG_900_913,
696
            },
697
            Identifier,
698
        },
699
    };
700
    use std::collections::HashMap;
701
    use std::path::PathBuf;
702
    use std::str::FromStr;
703

704
    #[tokio::test]
1✔
705
    async fn multi_point() -> Result<()> {
1✔
706
        let points = MultiPointCollection::from_data(
1✔
707
            MultiPoint::many(vec![
1✔
708
                MARBURG_EPSG_4326,
1✔
709
                COLOGNE_EPSG_4326,
1✔
710
                HAMBURG_EPSG_4326,
1✔
711
            ])
1✔
712
            .unwrap(),
1✔
713
            vec![TimeInterval::new_unchecked(0, 1); 3],
1✔
714
            Default::default(),
1✔
715
            CacheHint::default(),
1✔
716
        )?;
1✔
717

718
        let expected = MultiPoint::many(vec![
1✔
719
            MARBURG_EPSG_900_913,
1✔
720
            COLOGNE_EPSG_900_913,
1✔
721
            HAMBURG_EPSG_900_913,
1✔
722
        ])
1✔
723
        .unwrap();
1✔
724

1✔
725
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
726

1✔
727
        let target_spatial_reference =
1✔
728
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
729

730
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
731
            params: ReprojectionParams {
1✔
732
                target_spatial_reference,
1✔
733
            },
1✔
734
            sources: SingleRasterOrVectorSource {
1✔
735
                source: point_source.into(),
1✔
736
            },
1✔
737
        })
1✔
738
        .initialize(
1✔
739
            WorkflowOperatorPath::initialize_root(),
1✔
740
            &MockExecutionContext::test_default(),
1✔
741
        )
1✔
742
        .await?;
×
743

744
        let query_processor = initialized_operator.query_processor()?;
1✔
745

746
        let query_processor = query_processor.multi_point().unwrap();
1✔
747

1✔
748
        let query_rectangle = VectorQueryRectangle {
1✔
749
            spatial_bounds: BoundingBox2D::new(
1✔
750
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
751
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
752
            )
1✔
753
            .unwrap(),
1✔
754
            time_interval: TimeInterval::default(),
1✔
755
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
756
            attributes: ColumnSelection::all(),
1✔
757
        };
1✔
758
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
759

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

762
        let result = query
1✔
763
            .map(Result::unwrap)
1✔
764
            .collect::<Vec<MultiPointCollection>>()
1✔
765
            .await;
×
766

767
        assert_eq!(result.len(), 1);
1✔
768

769
        result[0]
1✔
770
            .geometries()
1✔
771
            .zip(expected.iter())
1✔
772
            .for_each(|(a, e)| {
3✔
773
                assert!(approx_eq!(&MultiPoint, &a.into(), e, epsilon = 0.00001));
3✔
774
            });
3✔
775

1✔
776
        Ok(())
1✔
777
    }
778

779
    #[tokio::test]
1✔
780
    async fn multi_lines() -> Result<()> {
1✔
781
        let lines = MultiLineStringCollection::from_data(
1✔
782
            vec![MultiLineString::new(vec![vec![
1✔
783
                MARBURG_EPSG_4326,
1✔
784
                COLOGNE_EPSG_4326,
1✔
785
                HAMBURG_EPSG_4326,
1✔
786
            ]])
1✔
787
            .unwrap()],
1✔
788
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
789
            Default::default(),
1✔
790
            CacheHint::default(),
1✔
791
        )?;
1✔
792

793
        let expected = [MultiLineString::new(vec![vec![
1✔
794
            MARBURG_EPSG_900_913,
1✔
795
            COLOGNE_EPSG_900_913,
1✔
796
            HAMBURG_EPSG_900_913,
1✔
797
        ]])
1✔
798
        .unwrap()];
1✔
799

1✔
800
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
801

1✔
802
        let target_spatial_reference =
1✔
803
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
804

805
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
806
            params: ReprojectionParams {
1✔
807
                target_spatial_reference,
1✔
808
            },
1✔
809
            sources: SingleRasterOrVectorSource {
1✔
810
                source: lines_source.into(),
1✔
811
            },
1✔
812
        })
1✔
813
        .initialize(
1✔
814
            WorkflowOperatorPath::initialize_root(),
1✔
815
            &MockExecutionContext::test_default(),
1✔
816
        )
1✔
817
        .await?;
×
818

819
        let query_processor = initialized_operator.query_processor()?;
1✔
820

821
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
822

1✔
823
        let query_rectangle = VectorQueryRectangle {
1✔
824
            spatial_bounds: BoundingBox2D::new(
1✔
825
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
826
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
827
            )
1✔
828
            .unwrap(),
1✔
829
            time_interval: TimeInterval::default(),
1✔
830
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
831
            attributes: ColumnSelection::all(),
1✔
832
        };
1✔
833
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
834

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

837
        let result = query
1✔
838
            .map(Result::unwrap)
1✔
839
            .collect::<Vec<MultiLineStringCollection>>()
1✔
840
            .await;
×
841

842
        assert_eq!(result.len(), 1);
1✔
843

844
        result[0]
1✔
845
            .geometries()
1✔
846
            .zip(expected.iter())
1✔
847
            .for_each(|(a, e)| {
1✔
848
                assert!(approx_eq!(
1✔
849
                    &MultiLineString,
1✔
850
                    &a.into(),
1✔
851
                    e,
1✔
852
                    epsilon = 0.00001
1✔
853
                ));
1✔
854
            });
1✔
855

1✔
856
        Ok(())
1✔
857
    }
858

859
    #[tokio::test]
1✔
860
    async fn multi_polygons() -> Result<()> {
1✔
861
        let polygons = MultiPolygonCollection::from_data(
1✔
862
            vec![MultiPolygon::new(vec![vec![vec![
1✔
863
                MARBURG_EPSG_4326,
1✔
864
                COLOGNE_EPSG_4326,
1✔
865
                HAMBURG_EPSG_4326,
1✔
866
                MARBURG_EPSG_4326,
1✔
867
            ]]])
1✔
868
            .unwrap()],
1✔
869
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
870
            Default::default(),
1✔
871
            CacheHint::default(),
1✔
872
        )?;
1✔
873

874
        let expected = [MultiPolygon::new(vec![vec![vec![
1✔
875
            MARBURG_EPSG_900_913,
1✔
876
            COLOGNE_EPSG_900_913,
1✔
877
            HAMBURG_EPSG_900_913,
1✔
878
            MARBURG_EPSG_900_913,
1✔
879
        ]]])
1✔
880
        .unwrap()];
1✔
881

1✔
882
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
883

1✔
884
        let target_spatial_reference =
1✔
885
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
886

887
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
888
            params: ReprojectionParams {
1✔
889
                target_spatial_reference,
1✔
890
            },
1✔
891
            sources: SingleRasterOrVectorSource {
1✔
892
                source: polygon_source.into(),
1✔
893
            },
1✔
894
        })
1✔
895
        .initialize(
1✔
896
            WorkflowOperatorPath::initialize_root(),
1✔
897
            &MockExecutionContext::test_default(),
1✔
898
        )
1✔
899
        .await?;
×
900

901
        let query_processor = initialized_operator.query_processor()?;
1✔
902

903
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
904

1✔
905
        let query_rectangle = VectorQueryRectangle {
1✔
906
            spatial_bounds: BoundingBox2D::new(
1✔
907
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
908
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
909
            )
1✔
910
            .unwrap(),
1✔
911
            time_interval: TimeInterval::default(),
1✔
912
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
913
            attributes: ColumnSelection::all(),
1✔
914
        };
1✔
915
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
916

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

919
        let result = query
1✔
920
            .map(Result::unwrap)
1✔
921
            .collect::<Vec<MultiPolygonCollection>>()
1✔
922
            .await;
×
923

924
        assert_eq!(result.len(), 1);
1✔
925

926
        result[0]
1✔
927
            .geometries()
1✔
928
            .zip(expected.iter())
1✔
929
            .for_each(|(a, e)| {
1✔
930
                assert!(approx_eq!(&MultiPolygon, &a.into(), e, epsilon = 0.00001));
1✔
931
            });
1✔
932

1✔
933
        Ok(())
1✔
934
    }
935

936
    #[tokio::test]
1✔
937
    async fn raster_identity() -> Result<()> {
1✔
938
        let projection = SpatialReference::new(
1✔
939
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
940
            4326,
1✔
941
        );
1✔
942

1✔
943
        let data = vec![
1✔
944
            RasterTile2D {
1✔
945
                time: TimeInterval::new_unchecked(0, 5),
1✔
946
                tile_position: [-1, 0].into(),
1✔
947
                band: 0,
1✔
948
                global_geo_transform: TestDefault::test_default(),
1✔
949
                grid_array: Grid::new([2, 2].into(), vec![1, 2, 3, 4]).unwrap().into(),
1✔
950
                properties: Default::default(),
1✔
951
                cache_hint: CacheHint::default(),
1✔
952
            },
1✔
953
            RasterTile2D {
1✔
954
                time: TimeInterval::new_unchecked(0, 5),
1✔
955
                tile_position: [-1, 1].into(),
1✔
956
                band: 0,
1✔
957
                global_geo_transform: TestDefault::test_default(),
1✔
958
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
959
                properties: Default::default(),
1✔
960
                cache_hint: CacheHint::default(),
1✔
961
            },
1✔
962
            RasterTile2D {
1✔
963
                time: TimeInterval::new_unchecked(5, 10),
1✔
964
                tile_position: [-1, 0].into(),
1✔
965
                band: 0,
1✔
966
                global_geo_transform: TestDefault::test_default(),
1✔
967
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
968
                    .unwrap()
1✔
969
                    .into(),
1✔
970
                properties: Default::default(),
1✔
971
                cache_hint: CacheHint::default(),
1✔
972
            },
1✔
973
            RasterTile2D {
1✔
974
                time: TimeInterval::new_unchecked(5, 10),
1✔
975
                tile_position: [-1, 1].into(),
1✔
976
                band: 0,
1✔
977
                global_geo_transform: TestDefault::test_default(),
1✔
978
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
979
                    .unwrap()
1✔
980
                    .into(),
1✔
981
                properties: Default::default(),
1✔
982
                cache_hint: CacheHint::default(),
1✔
983
            },
1✔
984
        ];
1✔
985

1✔
986
        let mrs1 = MockRasterSource {
1✔
987
            params: MockRasterSourceParams {
1✔
988
                data: data.clone(),
1✔
989
                result_descriptor: RasterResultDescriptor {
1✔
990
                    data_type: RasterDataType::U8,
1✔
991
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
992
                    time: None,
1✔
993
                    bbox: None,
1✔
994
                    resolution: Some(SpatialResolution::one()),
1✔
995
                    bands: RasterBandDescriptors::new_single_band(),
1✔
996
                },
1✔
997
            },
1✔
998
        }
1✔
999
        .boxed();
1✔
1000

1✔
1001
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1002
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
1✔
1003
            // we need a smaller tile size
1✔
1004
            shape_array: [2, 2],
1✔
1005
        };
1✔
1006

1✔
1007
        let query_ctx = MockQueryContext::test_default();
1✔
1008

1009
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1010
            params: ReprojectionParams {
1✔
1011
                target_spatial_reference: projection, // This test will do a identity reprojection
1✔
1012
            },
1✔
1013
            sources: SingleRasterOrVectorSource {
1✔
1014
                source: mrs1.into(),
1✔
1015
            },
1✔
1016
        })
1✔
1017
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1018
        .await?;
×
1019

1020
        let qp = initialized_operator
1✔
1021
            .query_processor()
1✔
1022
            .unwrap()
1✔
1023
            .get_u8()
1✔
1024
            .unwrap();
1✔
1025

1✔
1026
        let query_rect = RasterQueryRectangle {
1✔
1027
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 1.).into(), (3., 0.).into()),
1✔
1028
            time_interval: TimeInterval::new_unchecked(0, 10),
1✔
1029
            spatial_resolution: SpatialResolution::one(),
1✔
1030
            attributes: BandSelection::first(),
1✔
1031
        };
1✔
1032

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

1035
        let res = a
1✔
1036
            .map(Result::unwrap)
1✔
1037
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1038
            .await;
8✔
1039
        assert!(data.tiles_equal_ignoring_cache_hint(&res));
1✔
1040

1041
        Ok(())
1✔
1042
    }
1043

1044
    #[tokio::test]
1✔
1045
    async fn raster_ndvi_3857() -> Result<()> {
1✔
1046
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1047
        let query_ctx = MockQueryContext::test_default();
1✔
1048
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1049
        exe_ctx.tiling_specification =
1✔
1050
            TilingSpecification::new((0.0, 0.0).into(), [450, 450].into());
1✔
1051

1✔
1052
        let output_shape: GridShape2D = [900, 1800].into();
1✔
1053
        let output_bounds =
1✔
1054
            SpatialPartition2D::new_unchecked((0., 20_000_000.).into(), (20_000_000., 0.).into());
1✔
1055
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001);
1✔
1056
        // 2014-01-01
1✔
1057

1✔
1058
        let gdal_op = GdalSource {
1✔
1059
            params: GdalSourceParameters { data: id.clone() },
1✔
1060
        }
1✔
1061
        .boxed();
1✔
1062

1✔
1063
        let projection = SpatialReference::new(
1✔
1064
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
1065
            3857,
1✔
1066
        );
1✔
1067

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

1079
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
1080
        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✔
1081
        let spatial_resolution =
1✔
1082
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1083

1✔
1084
        let qp = initialized_operator
1✔
1085
            .query_processor()
1✔
1086
            .unwrap()
1✔
1087
            .get_u8()
1✔
1088
            .unwrap();
1✔
1089

1090
        let qs = qp
1✔
1091
            .raster_query(
1✔
1092
                RasterQueryRectangle {
1✔
1093
                    spatial_bounds: output_bounds,
1✔
1094
                    time_interval,
1✔
1095
                    spatial_resolution,
1✔
1096
                    attributes: BandSelection::first(),
1✔
1097
                },
1✔
1098
                &query_ctx,
1✔
1099
            )
1✔
1100
            .await
×
1101
            .unwrap();
1✔
1102

1103
        let res = qs
1✔
1104
            .map(Result::unwrap)
1✔
1105
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1106
            .await;
124✔
1107

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

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

1116
        assert_eq!(
1✔
1117
            include_bytes!(
1✔
1118
                "../../../test_data/raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01_tile-20.rst"
1✔
1119
            ) as &[u8],
1✔
1120
            res[8].clone().into_materialized_tile().grid_array.inner_grid.data.as_slice()
1✔
1121
        );
1✔
1122

1123
        Ok(())
1✔
1124
    }
1125

1126
    #[test]
1✔
1127
    fn query_rewrite_4326_3857() {
1✔
1128
        let query = VectorQueryRectangle {
1✔
1129
            spatial_bounds: BoundingBox2D::new_unchecked((-180., -90.).into(), (180., 90.).into()),
1✔
1130
            time_interval: TimeInterval::default(),
1✔
1131
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1132
            attributes: ColumnSelection::all(),
1✔
1133
        };
1✔
1134

1✔
1135
        let expected = BoundingBox2D::new_unchecked(
1✔
1136
            (-20_037_508.342_789_244, -20_048_966.104_014_594).into(),
1✔
1137
            (20_037_508.342_789_244, 20_048_966.104_014_594).into(),
1✔
1138
        );
1✔
1139

1✔
1140
        let reprojected = reproject_query(
1✔
1141
            query,
1✔
1142
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857),
1✔
1143
            SpatialReference::epsg_4326(),
1✔
1144
        )
1✔
1145
        .unwrap()
1✔
1146
        .unwrap();
1✔
1147

1✔
1148
        assert!(approx_eq!(
1✔
1149
            BoundingBox2D,
1✔
1150
            expected,
1✔
1151
            reprojected.spatial_bounds,
1✔
1152
            epsilon = 0.000_001
1✔
1153
        ));
1✔
1154
    }
1✔
1155

1156
    #[tokio::test]
1✔
1157
    async fn raster_ndvi_3857_to_4326() -> Result<()> {
1✔
1158
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1159
        let query_ctx = MockQueryContext::test_default();
1✔
1160

1✔
1161
        let m = GdalMetaDataRegular {
1✔
1162
            data_time: TimeInterval::new_unchecked(
1✔
1163
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1164
                TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1165
            ),
1✔
1166
            step: TimeStep {
1✔
1167
                granularity: TimeGranularity::Months,
1✔
1168
                step: 1,
1✔
1169
            },
1✔
1170
            time_placeholders: hashmap! {
1✔
1171
                "%_START_TIME_%".to_string() => GdalSourceTimePlaceholder {
1✔
1172
                    format: DateTimeParseFormat::custom("%Y-%m-%d".to_string()),
1✔
1173
                    reference: TimeReference::Start,
1✔
1174
                },
1✔
1175
            },
1✔
1176
            params: GdalDatasetParameters {
1✔
1177
                file_path: test_data!(
1✔
1178
                    "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_%_START_TIME_%.TIFF"
1✔
1179
                )
1✔
1180
                .into(),
1✔
1181
                rasterband_channel: 1,
1✔
1182
                geo_transform: GdalDatasetGeoTransform {
1✔
1183
                    origin_coordinate: (-20_037_508.342_789_244, 19_971_868.880_408_563).into(),
1✔
1184
                    x_pixel_size: 14_052.950_258_048_739,
1✔
1185
                    y_pixel_size: -14_057.881_117_788_405,
1✔
1186
                },
1✔
1187
                width: 2851,
1✔
1188
                height: 2841,
1✔
1189
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1190
                no_data_value: Some(0.),
1✔
1191
                properties_mapping: None,
1✔
1192
                gdal_open_options: None,
1✔
1193
                gdal_config_options: None,
1✔
1194
                allow_alphaband_as_mask: true,
1✔
1195
                retry: None,
1✔
1196
            },
1✔
1197
            result_descriptor: RasterResultDescriptor {
1✔
1198
                data_type: RasterDataType::U8,
1✔
1199
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857)
1✔
1200
                    .into(),
1✔
1201
                time: None,
1✔
1202
                bbox: None,
1✔
1203
                resolution: None,
1✔
1204
                bands: RasterBandDescriptors::new_single_band(),
1✔
1205
            },
1✔
1206
            cache_ttl: CacheTtlSeconds::default(),
1✔
1207
        };
1✔
1208

1✔
1209
        let id: DataId = DatasetId::new().into();
1✔
1210
        let name = NamedData::with_system_name("ndvi");
1✔
1211
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1212

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

1✔
1215
        let output_bounds =
1✔
1216
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1217
        let time_interval = TimeInterval::new_unchecked(1_396_310_400_000, 1_396_310_400_000);
1✔
1218
        // 2014-04-01
1✔
1219

1✔
1220
        let gdal_op = GdalSource {
1✔
1221
            params: GdalSourceParameters { data: name },
1✔
1222
        }
1✔
1223
        .boxed();
1✔
1224

1225
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1226
            params: ReprojectionParams {
1✔
1227
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1228
            },
1✔
1229
            sources: SingleRasterOrVectorSource {
1✔
1230
                source: gdal_op.into(),
1✔
1231
            },
1✔
1232
        })
1✔
1233
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1234
        .await?;
×
1235

1236
        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✔
1237
        let y_query_resolution = output_bounds.size_y() / 240.; // *2 to account for the dataset aspect ratio 2:1
1✔
1238
        let spatial_resolution =
1✔
1239
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1240

1✔
1241
        let qp = initialized_operator
1✔
1242
            .query_processor()
1✔
1243
            .unwrap()
1✔
1244
            .get_u8()
1✔
1245
            .unwrap();
1✔
1246

1247
        let qs = qp
1✔
1248
            .raster_query(
1✔
1249
                QueryRectangle {
1✔
1250
                    spatial_bounds: output_bounds,
1✔
1251
                    time_interval,
1✔
1252
                    spatial_resolution,
1✔
1253
                    attributes: BandSelection::first(),
1✔
1254
                },
1✔
1255
                &query_ctx,
1✔
1256
            )
1✔
1257
            .await
×
1258
            .unwrap();
1✔
1259

1260
        let tiles = qs
1✔
1261
            .map(Result::unwrap)
1✔
1262
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1263
            .await;
244✔
1264

1265
        // the test must generate 8x4 tiles
1266
        assert_eq!(tiles.len(), 32);
1✔
1267

1268
        // none of the tiles should be empty
1269
        assert!(tiles.iter().all(|t| !t.is_empty()));
32✔
1270

1271
        Ok(())
1✔
1272
    }
1273

1274
    #[test]
1✔
1275
    fn source_resolution() {
1✔
1276
        let epsg_4326 = SpatialReference::epsg_4326();
1✔
1277
        let epsg_3857 = SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857);
1✔
1278

1✔
1279
        // use ndvi dataset that was reprojected using gdal as ground truth
1✔
1280
        let dataset_4326 = gdal_open_dataset(test_data!(
1✔
1281
            "raster/modis_ndvi/MOD13A2_M_NDVI_2014-04-01.TIFF"
1✔
1282
        ))
1✔
1283
        .unwrap();
1✔
1284
        let geotransform_4326 = dataset_4326.geo_transform().unwrap();
1✔
1285
        let res_4326 = SpatialResolution::new(geotransform_4326[1], -geotransform_4326[5]).unwrap();
1✔
1286

1✔
1287
        let dataset_3857 = gdal_open_dataset(test_data!(
1✔
1288
            "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01.TIFF"
1✔
1289
        ))
1✔
1290
        .unwrap();
1✔
1291
        let geotransform_3857 = dataset_3857.geo_transform().unwrap();
1✔
1292
        let res_3857 = SpatialResolution::new(geotransform_3857[1], -geotransform_3857[5]).unwrap();
1✔
1293

1✔
1294
        // ndvi was projected from 4326 to 3857. The calculated source_resolution for getting the raster in 3857 with `res_3857`
1✔
1295
        // should thus roughly be like the original `res_4326`
1✔
1296
        let result_res = suggest_pixel_size_from_diag_cross_projected::<SpatialPartition2D>(
1✔
1297
            epsg_3857.area_of_use_projected().unwrap(),
1✔
1298
            epsg_4326.area_of_use_projected().unwrap(),
1✔
1299
            res_3857,
1✔
1300
        )
1✔
1301
        .unwrap();
1✔
1302
        assert!(1. - (result_res.x / res_4326.x).abs() < 0.02);
1✔
1303
        assert!(1. - (result_res.y / res_4326.y).abs() < 0.02);
1✔
1304
    }
1✔
1305

1306
    #[tokio::test]
1✔
1307
    async fn query_outside_projection_area_of_use_produces_empty_tiles() {
1✔
1308
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1309
        let query_ctx = MockQueryContext::test_default();
1✔
1310

1✔
1311
        let m = GdalMetaDataStatic {
1✔
1312
            time: Some(TimeInterval::default()),
1✔
1313
            params: GdalDatasetParameters {
1✔
1314
                file_path: PathBuf::new(),
1✔
1315
                rasterband_channel: 1,
1✔
1316
                geo_transform: GdalDatasetGeoTransform {
1✔
1317
                    origin_coordinate: (166_021.44, 9_329_005.188).into(),
1✔
1318
                    x_pixel_size: (534_994.66 - 166_021.444) / 100.,
1✔
1319
                    y_pixel_size: -9_329_005.18 / 100.,
1✔
1320
                },
1✔
1321
                width: 100,
1✔
1322
                height: 100,
1✔
1323
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1324
                no_data_value: Some(0.),
1✔
1325
                properties_mapping: None,
1✔
1326
                gdal_open_options: None,
1✔
1327
                gdal_config_options: None,
1✔
1328
                allow_alphaband_as_mask: true,
1✔
1329
                retry: None,
1✔
1330
            },
1✔
1331
            result_descriptor: RasterResultDescriptor {
1✔
1332
                data_type: RasterDataType::U8,
1✔
1333
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636)
1✔
1334
                    .into(),
1✔
1335
                time: None,
1✔
1336
                bbox: None,
1✔
1337
                resolution: None,
1✔
1338
                bands: RasterBandDescriptors::new_single_band(),
1✔
1339
            },
1✔
1340
            cache_ttl: CacheTtlSeconds::default(),
1✔
1341
        };
1✔
1342

1✔
1343
        let id: DataId = DatasetId::new().into();
1✔
1344
        let name = NamedData::with_system_name("ndvi");
1✔
1345
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1346

1✔
1347
        exe_ctx.tiling_specification =
1✔
1348
            TilingSpecification::new((0.0, 0.0).into(), [600, 600].into());
1✔
1349

1✔
1350
        let output_shape: GridShape2D = [1000, 1000].into();
1✔
1351
        let output_bounds =
1✔
1352
            SpatialPartition2D::new_unchecked((-180., 0.).into(), (180., -90.).into());
1✔
1353
        let time_interval = TimeInterval::new_instant(1_388_534_400_000).unwrap(); // 2014-01-01
1✔
1354

1✔
1355
        let gdal_op = GdalSource {
1✔
1356
            params: GdalSourceParameters { data: name },
1✔
1357
        }
1✔
1358
        .boxed();
1✔
1359

1360
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1361
            params: ReprojectionParams {
1✔
1362
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1363
            },
1✔
1364
            sources: SingleRasterOrVectorSource {
1✔
1365
                source: gdal_op.into(),
1✔
1366
            },
1✔
1367
        })
1✔
1368
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1369
        .await
×
1370
        .unwrap();
1✔
1371

1✔
1372
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
1373
        let y_query_resolution = output_bounds.size_y() / (output_shape.axis_size_y()) as f64;
1✔
1374
        let spatial_resolution =
1✔
1375
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1376

1✔
1377
        let qp = initialized_operator
1✔
1378
            .query_processor()
1✔
1379
            .unwrap()
1✔
1380
            .get_u8()
1✔
1381
            .unwrap();
1✔
1382

1383
        let result = qp
1✔
1384
            .raster_query(
1✔
1385
                QueryRectangle {
1✔
1386
                    spatial_bounds: output_bounds,
1✔
1387
                    time_interval,
1✔
1388
                    spatial_resolution,
1✔
1389
                    attributes: BandSelection::first(),
1✔
1390
                },
1✔
1391
                &query_ctx,
1✔
1392
            )
1✔
1393
            .await
×
1394
            .unwrap()
1✔
1395
            .map(Result::unwrap)
1✔
1396
            .collect::<Vec<_>>()
1✔
1397
            .await;
×
1398

1399
        assert_eq!(result.len(), 4);
1✔
1400

1401
        for r in result {
5✔
1402
            assert!(r.is_empty());
4✔
1403
        }
1404
    }
1405

1406
    #[tokio::test]
1✔
1407
    async fn points_from_wgs84_to_utm36n() {
1✔
1408
        let exe_ctx = MockExecutionContext::test_default();
1✔
1409
        let query_ctx = MockQueryContext::test_default();
1✔
1410

1✔
1411
        let point_source = MockFeatureCollectionSource::single(
1✔
1412
            MultiPointCollection::from_data(
1✔
1413
                MultiPoint::many(vec![
1✔
1414
                    vec![(30.0, 0.0)], // lower left of utm36n area of use
1✔
1415
                    vec![(36.0, 84.0)],
1✔
1416
                    vec![(33.0, 42.0)], // upper right of utm36n area of use
1✔
1417
                ])
1✔
1418
                .unwrap(),
1✔
1419
                vec![TimeInterval::default(); 3],
1✔
1420
                HashMap::default(),
1✔
1421
                CacheHint::default(),
1✔
1422
            )
1✔
1423
            .unwrap(),
1✔
1424
        )
1✔
1425
        .boxed();
1✔
1426

1427
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1428
            params: ReprojectionParams {
1✔
1429
                target_spatial_reference: SpatialReference::new(
1✔
1430
                    SpatialReferenceAuthority::Epsg,
1✔
1431
                    32636, // utm36n
1✔
1432
                ),
1✔
1433
            },
1✔
1434
            sources: SingleRasterOrVectorSource {
1✔
1435
                source: point_source.into(),
1✔
1436
            },
1✔
1437
        })
1✔
1438
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1439
        .await
×
1440
        .unwrap();
1✔
1441

1✔
1442
        let qp = initialized_operator
1✔
1443
            .query_processor()
1✔
1444
            .unwrap()
1✔
1445
            .multi_point()
1✔
1446
            .unwrap();
1✔
1447

1✔
1448
        let spatial_bounds = BoundingBox2D::new(
1✔
1449
            (166_021.44, 0.00).into(), // lower left of projected utm36n area of use
1✔
1450
            (534_994.666_6, 9_329_005.18).into(), // upper right of projected utm36n area of use
1✔
1451
        )
1✔
1452
        .unwrap();
1✔
1453

1454
        let qs = qp
1✔
1455
            .vector_query(
1✔
1456
                QueryRectangle {
1✔
1457
                    spatial_bounds,
1✔
1458
                    time_interval: TimeInterval::default(),
1✔
1459
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1460
                    attributes: ColumnSelection::all(),
1✔
1461
                },
1✔
1462
                &query_ctx,
1✔
1463
            )
1✔
1464
            .await
×
1465
            .unwrap();
1✔
1466

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

1469
        assert_eq!(points.len(), 1);
1✔
1470

1471
        let points = &points[0];
1✔
1472

1✔
1473
        assert_eq!(
1✔
1474
            points.coordinates(),
1✔
1475
            &[
1✔
1476
                (166_021.443_080_538_42, 0.0).into(),
1✔
1477
                (534_994.655_061_136_1, 9_329_005.182_447_437).into(),
1✔
1478
                (499_999.999_999_999_5, 4_649_776.224_819_178).into()
1✔
1479
            ]
1✔
1480
        );
1✔
1481
    }
1482

1483
    #[tokio::test]
1✔
1484
    async fn points_from_utm36n_to_wgs84() {
1✔
1485
        let exe_ctx = MockExecutionContext::test_default();
1✔
1486
        let query_ctx = MockQueryContext::test_default();
1✔
1487

1✔
1488
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1489
            vec![MultiPointCollection::from_data(
1✔
1490
                MultiPoint::many(vec![
1✔
1491
                    vec![(166_021.443_080_538_42, 0.0)],
1✔
1492
                    vec![(534_994.655_061_136_1, 9_329_005.182_447_437)],
1✔
1493
                    vec![(499_999.999_999_999_5, 4_649_776.224_819_178)],
1✔
1494
                ])
1✔
1495
                .unwrap(),
1✔
1496
                vec![TimeInterval::default(); 3],
1✔
1497
                HashMap::default(),
1✔
1498
                CacheHint::default(),
1✔
1499
            )
1✔
1500
            .unwrap()],
1✔
1501
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1502
        )
1✔
1503
        .boxed();
1✔
1504

1505
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1506
            params: ReprojectionParams {
1✔
1507
                target_spatial_reference: SpatialReference::new(
1✔
1508
                    SpatialReferenceAuthority::Epsg,
1✔
1509
                    4326, // utm36n
1✔
1510
                ),
1✔
1511
            },
1✔
1512
            sources: SingleRasterOrVectorSource {
1✔
1513
                source: point_source.into(),
1✔
1514
            },
1✔
1515
        })
1✔
1516
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1517
        .await
×
1518
        .unwrap();
1✔
1519

1✔
1520
        let qp = initialized_operator
1✔
1521
            .query_processor()
1✔
1522
            .unwrap()
1✔
1523
            .multi_point()
1✔
1524
            .unwrap();
1✔
1525

1✔
1526
        let spatial_bounds = BoundingBox2D::new(
1✔
1527
            (30.0, 0.0).into(),  // lower left of utm36n area of use
1✔
1528
            (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1529
        )
1✔
1530
        .unwrap();
1✔
1531

1532
        let qs = qp
1✔
1533
            .vector_query(
1✔
1534
                QueryRectangle {
1✔
1535
                    spatial_bounds,
1✔
1536
                    time_interval: TimeInterval::default(),
1✔
1537
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1538
                    attributes: ColumnSelection::all(),
1✔
1539
                },
1✔
1540
                &query_ctx,
1✔
1541
            )
1✔
1542
            .await
×
1543
            .unwrap();
1✔
1544

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

1547
        assert_eq!(points.len(), 1);
1✔
1548

1549
        let points = &points[0];
1✔
1550

1✔
1551
        assert!(approx_eq!(
1✔
1552
            &[Coordinate2D],
1✔
1553
            points.coordinates(),
1✔
1554
            &[
1✔
1555
                (30.0, 0.0).into(), // lower left of utm36n area of use
1✔
1556
                (36.0, 84.0).into(),
1✔
1557
                (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1558
            ]
1✔
1559
        ));
1✔
1560
    }
1561

1562
    #[tokio::test]
1✔
1563
    async fn points_from_utm36n_to_wgs84_out_of_area() {
1✔
1564
        // 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✔
1565

1✔
1566
        let exe_ctx = MockExecutionContext::test_default();
1✔
1567
        let query_ctx = MockQueryContext::test_default();
1✔
1568

1✔
1569
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1570
            vec![MultiPointCollection::from_data(
1✔
1571
                MultiPoint::many(vec![
1✔
1572
                    vec![(758_565., 4_928_353.)], // (12.25, 44,46)
1✔
1573
                ])
1✔
1574
                .unwrap(),
1✔
1575
                vec![TimeInterval::default(); 1],
1✔
1576
                HashMap::default(),
1✔
1577
                CacheHint::default(),
1✔
1578
            )
1✔
1579
            .unwrap()],
1✔
1580
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1581
        )
1✔
1582
        .boxed();
1✔
1583

1584
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1585
            params: ReprojectionParams {
1✔
1586
                target_spatial_reference: SpatialReference::new(
1✔
1587
                    SpatialReferenceAuthority::Epsg,
1✔
1588
                    4326, // utm36n
1✔
1589
                ),
1✔
1590
            },
1✔
1591
            sources: SingleRasterOrVectorSource {
1✔
1592
                source: point_source.into(),
1✔
1593
            },
1✔
1594
        })
1✔
1595
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1596
        .await
×
1597
        .unwrap();
1✔
1598

1✔
1599
        let qp = initialized_operator
1✔
1600
            .query_processor()
1✔
1601
            .unwrap()
1✔
1602
            .multi_point()
1✔
1603
            .unwrap();
1✔
1604

1✔
1605
        let spatial_bounds = BoundingBox2D::new(
1✔
1606
            (10.0, 0.0).into(),  // -20 x values left of lower left of utm36n area of use
1✔
1607
            (13.0, 42.0).into(), // -20 x values left of upper right of utm36n area of use
1✔
1608
        )
1✔
1609
        .unwrap();
1✔
1610

1611
        let qs = qp
1✔
1612
            .vector_query(
1✔
1613
                QueryRectangle {
1✔
1614
                    spatial_bounds,
1✔
1615
                    time_interval: TimeInterval::default(),
1✔
1616
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1617
                    attributes: ColumnSelection::all(),
1✔
1618
                },
1✔
1619
                &query_ctx,
1✔
1620
            )
1✔
1621
            .await
×
1622
            .unwrap();
1✔
1623

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

1626
        assert_eq!(points.len(), 1);
1✔
1627

1628
        let points = &points[0];
1✔
1629

1✔
1630
        assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1✔
1631
        assert!(points.coordinates().is_empty());
1✔
1632
    }
1633

1634
    #[test]
1✔
1635
    fn it_derives_raster_result_descriptor() {
1✔
1636
        let in_proj = SpatialReference::epsg_4326();
1✔
1637
        let out_proj = SpatialReference::from_str("EPSG:3857").unwrap();
1✔
1638
        let bbox = Some(SpatialPartition2D::new_unchecked(
1✔
1639
            (-180., 90.).into(),
1✔
1640
            (180., -90.).into(),
1✔
1641
        ));
1✔
1642

1✔
1643
        let resolution = Some(SpatialResolution::new_unchecked(0.1, 0.1));
1✔
1644

1✔
1645
        let (in_bounds, out_bounds, out_res) =
1✔
1646
            InitializedRasterReprojection::derive_raster_in_bounds_out_bounds_out_res(
1✔
1647
                in_proj, out_proj, resolution, bbox,
1✔
1648
            )
1✔
1649
            .unwrap();
1✔
1650

1✔
1651
        assert_eq!(
1✔
1652
            in_bounds.unwrap(),
1✔
1653
            SpatialPartition2D::new_unchecked((-180., 85.06).into(), (180., -85.06).into(),)
1✔
1654
        );
1✔
1655

1656
        assert_eq!(
1✔
1657
            out_bounds.unwrap(),
1✔
1658
            out_proj
1✔
1659
                .area_of_use_projected::<SpatialPartition2D>()
1✔
1660
                .unwrap()
1✔
1661
        );
1✔
1662

1663
        // TODO: y resolution should be double the x resolution, but currently we only compute a uniform resolution
1664
        assert_eq!(
1✔
1665
            out_res.unwrap(),
1✔
1666
            SpatialResolution::new_unchecked(14_237.781_884_528_267, 14_237.781_884_528_267),
1✔
1667
        );
1✔
1668
    }
1✔
1669
}
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