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

geo-engine / geoengine / 17203723508

25 Aug 2025 08:37AM UTC coverage: 88.646% (+0.001%) from 88.645%
17203723508

push

github

web-flow
feat(datatypes): reproject outside area of use (#1071)

* Reproject outside area of use

Combines clipped and non-clipped reprojection of border sample points.
The largest possible bbox (a combination of both or the one which is non-empty) is taken as result.

* Re-enable previous behavior for plot and raster

105 of 112 new or added lines in 5 files covered. (93.75%)

6 existing lines in 2 files now uncovered.

112460 of 126864 relevant lines covered (88.65%)

79489.73 hits per line

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

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

3
use super::map_query::MapQueryProcessor;
4
use crate::{
5
    adapters::{
6
        FillerTileCacheExpirationStrategy, FillerTimeBounds, RasterSubQueryAdapter,
7
        SparseTilesFillAdapter, TileReprojectionSubQuery, fold_by_coordinate_lookup_future,
8
    },
9
    engine::{
10
        CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources,
11
        InitializedVectorOperator, Operator, OperatorName, QueryContext, QueryProcessor,
12
        RasterOperator, RasterQueryProcessor, RasterResultDescriptor, SingleRasterOrVectorSource,
13
        TypedRasterQueryProcessor, TypedVectorQueryProcessor, VectorOperator, VectorQueryProcessor,
14
        VectorResultDescriptor, WorkflowOperatorPath,
15
    },
16
    error::{self, Error},
17
    util::Result,
18
};
19
use async_trait::async_trait;
20
use futures::stream::BoxStream;
21
use futures::{StreamExt, stream};
22
use geoengine_datatypes::{
23
    collections::FeatureCollection,
24
    operations::reproject::{
25
        CoordinateProjection, CoordinateProjector, Reproject, ReprojectClipped,
26
        reproject_and_unify_bbox, reproject_query, suggest_pixel_size_from_diag_cross_projected,
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

38
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
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
    name: CanonicOperatorName,
60
    path: WorkflowOperatorPath,
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
    path: WorkflowOperatorPath,
70
    result_descriptor: RasterResultDescriptor,
71
    source: Box<dyn InitializedRasterOperator>,
72
    state: Option<ReprojectionBounds>,
73
    source_srs: SpatialReference,
74
    target_srs: SpatialReference,
75
    tiling_spec: TilingSpecification,
76
}
77

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

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

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

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

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

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

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

134
        let in_srs = Into::<Option<SpatialReference>>::into(in_desc.spatial_reference)
6✔
135
            .ok_or(Error::AllSourcesMustHaveSameSpatialReference)?;
6✔
136

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

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

154
        let state = match (in_bounds, out_bounds) {
4✔
155
            (Some(in_bounds), Some(out_bounds)) => Some(ReprojectionBounds {
4✔
156
                valid_in_bounds: in_bounds,
4✔
157
                valid_out_bounds: out_bounds,
4✔
158
            }),
4✔
159
            _ => None,
×
160
        };
161

162
        Ok(InitializedRasterReprojection {
4✔
163
            name,
4✔
164
            path,
4✔
165
            result_descriptor,
4✔
166
            source: source_raster_operator,
4✔
167
            state,
4✔
168
            source_srs: in_srs,
4✔
169
            target_srs: params.target_spatial_reference,
4✔
170
            tiling_spec,
4✔
171
        })
4✔
172
    }
6✔
173

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

191
            (valid_bounds_in, valid_bounds_out)
3✔
192
        };
193

194
        let out_res = match (source_spatial_resolution, in_bbox, out_bbox) {
5✔
195
            (Some(in_res), Some(in_bbox), Some(out_bbox)) => {
3✔
196
                suggest_pixel_size_from_diag_cross_projected(in_bbox, out_bbox, in_res).ok()
3✔
197
            }
198
            _ => None,
2✔
199
        };
200

201
        Ok((in_bbox, out_bbox, out_res))
5✔
202
    }
7✔
203
}
204

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

215
        let vector_source =
6✔
216
            self.sources
7✔
217
                .vector()
7✔
218
                .ok_or_else(|| error::Error::InvalidOperatorType {
7✔
219
                    expected: "Vector".to_owned(),
1✔
220
                    found: "Raster".to_owned(),
1✔
221
                })?;
1✔
222

223
        let initialized_source = vector_source
6✔
224
            .initialize_sources(path.clone(), context)
6✔
225
            .await?;
6✔
226

227
        let initialized_operator = InitializedVectorReprojection::try_new_with_input(
6✔
228
            name,
6✔
229
            path,
6✔
230
            self.params,
6✔
231
            initialized_source.vector,
6✔
232
        )?;
×
233

234
        Ok(initialized_operator.boxed())
6✔
235
    }
14✔
236

237
    span_fn!(Reprojection);
238
}
239

240
impl InitializedVectorOperator for InitializedVectorReprojection {
241
    fn result_descriptor(&self) -> &VectorResultDescriptor {
×
242
        &self.result_descriptor
×
243
    }
×
244

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

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

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

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

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

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

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

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

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

381
    fn result_descriptor(&self) -> &VectorResultDescriptor {
12✔
382
        &self.result_descriptor
12✔
383
    }
12✔
384
}
385

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

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

404
        let initialized_source = raster_source
6✔
405
            .initialize_sources(path.clone(), context)
6✔
406
            .await?;
6✔
407

408
        let initialized_operator = InitializedRasterReprojection::try_new_with_input(
6✔
409
            name,
6✔
410
            path,
6✔
411
            self.params,
6✔
412
            initialized_source.raster,
6✔
413
            context.tiling_specification(),
6✔
414
        )?;
2✔
415

416
        Ok(initialized_operator.boxed())
4✔
417
    }
12✔
418

419
    span_fn!(Reprojection);
420
}
421

422
impl InitializedRasterOperator for InitializedRasterReprojection {
423
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
424
        &self.result_descriptor
×
425
    }
×
426

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

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

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

567
    fn canonic_name(&self) -> CanonicOperatorName {
×
568
        self.name.clone()
×
569
    }
×
570

571
    fn name(&self) -> &'static str {
×
572
        Reprojection::TYPE_NAME
×
573
    }
×
574

575
    fn path(&self) -> WorkflowOperatorPath {
×
576
        self.path.clone()
×
577
    }
×
578
}
579

580
pub struct RasterReprojectionProcessor<Q, P>
581
where
582
    Q: RasterQueryProcessor<RasterType = P>,
583
{
584
    source: Q,
585
    result_descriptor: RasterResultDescriptor,
586
    from: SpatialReference,
587
    to: SpatialReference,
588
    tiling_spec: TilingSpecification,
589
    state: Option<ReprojectionBounds>,
590
    _phantom_data: PhantomData<P>,
591
}
592

593
impl<Q, P> RasterReprojectionProcessor<Q, P>
594
where
595
    Q: QueryProcessor<
596
            Output = RasterTile2D<P>,
597
            SpatialBounds = SpatialPartition2D,
598
            Selection = BandSelection,
599
            ResultDescription = RasterResultDescriptor,
600
        >,
601
    P: Pixel,
602
{
603
    pub fn new(
4✔
604
        source: Q,
4✔
605
        result_descriptor: RasterResultDescriptor,
4✔
606
        from: SpatialReference,
4✔
607
        to: SpatialReference,
4✔
608
        tiling_spec: TilingSpecification,
4✔
609
        state: Option<ReprojectionBounds>,
4✔
610
    ) -> Self {
4✔
611
        Self {
4✔
612
            source,
4✔
613
            result_descriptor,
4✔
614
            from,
4✔
615
            to,
4✔
616
            tiling_spec,
4✔
617
            state,
4✔
618
            _phantom_data: PhantomData,
4✔
619
        }
4✔
620
    }
4✔
621
}
622

623
#[async_trait]
624
impl<Q, P> QueryProcessor for RasterReprojectionProcessor<Q, P>
625
where
626
    Q: QueryProcessor<
627
            Output = RasterTile2D<P>,
628
            SpatialBounds = SpatialPartition2D,
629
            Selection = BandSelection,
630
            ResultDescription = RasterResultDescriptor,
631
        >,
632
    P: Pixel,
633
{
634
    type Output = RasterTile2D<P>;
635
    type SpatialBounds = SpatialPartition2D;
636
    type Selection = BandSelection;
637
    type ResultDescription = RasterResultDescriptor;
638

639
    async fn _query<'a>(
640
        &'a self,
641
        query: RasterQueryRectangle,
642
        ctx: &'a dyn QueryContext,
643
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
8✔
644
        if let Some(state) = &self.state {
4✔
645
            let valid_bounds_in = state.valid_in_bounds;
4✔
646
            let valid_bounds_out = state.valid_out_bounds;
4✔
647

648
            // calculate the spatial resolution the input data should have using the intersection and the requested resolution
649
            let in_spatial_res = suggest_pixel_size_from_diag_cross_projected(
4✔
650
                valid_bounds_out,
4✔
651
                valid_bounds_in,
4✔
652
                query.spatial_resolution,
4✔
653
            )?;
×
654

655
            // setup the subquery
656
            let sub_query_spec = TileReprojectionSubQuery {
4✔
657
                in_srs: self.from,
4✔
658
                out_srs: self.to,
4✔
659
                fold_fn: fold_by_coordinate_lookup_future,
4✔
660
                in_spatial_res,
4✔
661
                valid_bounds_in,
4✔
662
                valid_bounds_out,
4✔
663
                _phantom_data: PhantomData,
4✔
664
            };
4✔
665

666
            // return the adapter which will reproject the tiles and uses the fill adapter to inject missing tiles
667
            Ok(RasterSubQueryAdapter::<'a, P, _, _>::new(
4✔
668
                &self.source,
4✔
669
                query,
4✔
670
                self.tiling_spec,
4✔
671
                ctx,
4✔
672
                sub_query_spec,
4✔
673
            )
4✔
674
            .filter_and_fill(FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles))
4✔
675
        } else {
676
            tracing::debug!("No intersection between source data / srs and target srs");
×
677

678
            let tiling_strat = self
×
679
                .tiling_spec
×
680
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
×
681

682
            let grid_bounds = tiling_strat.tile_grid_box(query.spatial_partition());
×
683
            Ok(Box::pin(SparseTilesFillAdapter::new(
×
684
                stream::empty(),
×
685
                grid_bounds,
×
686
                query.attributes.count(),
×
687
                tiling_strat.geo_transform,
×
688
                self.tiling_spec.tile_size_in_pixels,
×
689
                FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles,
×
690
                query.time_interval,
×
691
                FillerTimeBounds::from(query.time_interval), // TODO: derive this from the query once the child query can provide this.
×
692
            )))
×
693
        }
694
    }
8✔
695

696
    fn result_descriptor(&self) -> &RasterResultDescriptor {
8✔
697
        &self.result_descriptor
8✔
698
    }
8✔
699
}
700

701
#[cfg(test)]
702
mod tests {
703
    use super::*;
704
    use crate::engine::{MockExecutionContext, MockQueryContext, RasterBandDescriptors};
705
    use crate::mock::MockFeatureCollectionSource;
706
    use crate::mock::{MockRasterSource, MockRasterSourceParams};
707
    use crate::{
708
        engine::{ChunkByteSize, VectorOperator},
709
        source::{
710
            FileNotFoundHandling, GdalDatasetGeoTransform, GdalDatasetParameters,
711
            GdalMetaDataRegular, GdalMetaDataStatic, GdalSource, GdalSourceParameters,
712
            GdalSourceTimePlaceholder, TimeReference,
713
        },
714
        test_data,
715
        util::gdal::{add_ndvi_dataset, gdal_open_dataset},
716
    };
717
    use float_cmp::approx_eq;
718
    use futures::StreamExt;
719
    use geoengine_datatypes::collections::IntoGeometryIterator;
720
    use geoengine_datatypes::dataset::NamedData;
721
    use geoengine_datatypes::primitives::{
722
        AxisAlignedRectangle, Coordinate2D, DateTimeParseFormat,
723
    };
724
    use geoengine_datatypes::primitives::{CacheHint, CacheTtlSeconds};
725
    use geoengine_datatypes::raster::TilesEqualIgnoringCacheHint;
726
    use geoengine_datatypes::{
727
        collections::{
728
            GeometryCollection, MultiLineStringCollection, MultiPointCollection,
729
            MultiPolygonCollection,
730
        },
731
        dataset::{DataId, DatasetId},
732
        hashmap,
733
        primitives::{
734
            BoundingBox2D, MultiLineString, MultiPoint, MultiPolygon, QueryRectangle,
735
            SpatialResolution, TimeGranularity, TimeInstance, TimeInterval, TimeStep,
736
        },
737
        raster::{Grid, GridShape, GridShape2D, GridSize, RasterDataType, RasterTile2D},
738
        spatial_reference::SpatialReferenceAuthority,
739
        util::{
740
            Identifier,
741
            test::TestDefault,
742
            well_known_data::{
743
                COLOGNE_EPSG_900_913, COLOGNE_EPSG_4326, HAMBURG_EPSG_900_913, HAMBURG_EPSG_4326,
744
                MARBURG_EPSG_900_913, MARBURG_EPSG_4326,
745
            },
746
        },
747
    };
748
    use std::collections::HashMap;
749
    use std::path::PathBuf;
750
    use std::str::FromStr;
751

752
    #[tokio::test]
753
    async fn multi_point() -> Result<()> {
1✔
754
        let points = MultiPointCollection::from_data(
1✔
755
            MultiPoint::many(vec![
1✔
756
                MARBURG_EPSG_4326,
757
                COLOGNE_EPSG_4326,
758
                HAMBURG_EPSG_4326,
759
            ])
760
            .unwrap(),
1✔
761
            vec![TimeInterval::new_unchecked(0, 1); 3],
1✔
762
            Default::default(),
1✔
763
            CacheHint::default(),
1✔
764
        )?;
×
765

766
        let expected = MultiPoint::many(vec![
1✔
767
            MARBURG_EPSG_900_913,
768
            COLOGNE_EPSG_900_913,
769
            HAMBURG_EPSG_900_913,
770
        ])
771
        .unwrap();
1✔
772

773
        let point_source = MockFeatureCollectionSource::single(points.clone()).boxed();
1✔
774

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

778
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
779
            params: ReprojectionParams {
1✔
780
                target_spatial_reference,
1✔
781
            },
1✔
782
            sources: SingleRasterOrVectorSource {
1✔
783
                source: point_source.into(),
1✔
784
            },
1✔
785
        })
1✔
786
        .initialize(
1✔
787
            WorkflowOperatorPath::initialize_root(),
1✔
788
            &MockExecutionContext::test_default(),
1✔
789
        )
1✔
790
        .await?;
1✔
791

792
        let query_processor = initialized_operator.query_processor()?;
1✔
793

794
        let query_processor = query_processor.multi_point().unwrap();
1✔
795

796
        let query_rectangle = VectorQueryRectangle {
1✔
797
            spatial_bounds: BoundingBox2D::new(
1✔
798
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
799
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
800
            )
1✔
801
            .unwrap(),
1✔
802
            time_interval: TimeInterval::default(),
1✔
803
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
804
            attributes: ColumnSelection::all(),
1✔
805
        };
1✔
806
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
807

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

810
        let result = query
1✔
811
            .map(Result::unwrap)
1✔
812
            .collect::<Vec<MultiPointCollection>>()
1✔
813
            .await;
1✔
814

815
        assert_eq!(result.len(), 1);
1✔
816

817
        result[0]
1✔
818
            .geometries()
1✔
819
            .zip(expected.iter())
1✔
820
            .for_each(|(a, e)| {
3✔
821
                assert!(approx_eq!(&MultiPoint, &a.into(), e, epsilon = 0.00001));
3✔
822
            });
3✔
823

824
        Ok(())
2✔
825
    }
1✔
826

827
    #[tokio::test]
828
    async fn multi_lines() -> Result<()> {
1✔
829
        let lines = MultiLineStringCollection::from_data(
1✔
830
            vec![
1✔
831
                MultiLineString::new(vec![vec![
1✔
832
                    MARBURG_EPSG_4326,
833
                    COLOGNE_EPSG_4326,
834
                    HAMBURG_EPSG_4326,
835
                ]])
836
                .unwrap(),
1✔
837
            ],
838
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
839
            Default::default(),
1✔
840
            CacheHint::default(),
1✔
841
        )?;
×
842

843
        let expected = [MultiLineString::new(vec![vec![
1✔
844
            MARBURG_EPSG_900_913,
1✔
845
            COLOGNE_EPSG_900_913,
1✔
846
            HAMBURG_EPSG_900_913,
1✔
847
        ]])
1✔
848
        .unwrap()];
1✔
849

850
        let lines_source = MockFeatureCollectionSource::single(lines.clone()).boxed();
1✔
851

852
        let target_spatial_reference =
1✔
853
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
854

855
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
856
            params: ReprojectionParams {
1✔
857
                target_spatial_reference,
1✔
858
            },
1✔
859
            sources: SingleRasterOrVectorSource {
1✔
860
                source: lines_source.into(),
1✔
861
            },
1✔
862
        })
1✔
863
        .initialize(
1✔
864
            WorkflowOperatorPath::initialize_root(),
1✔
865
            &MockExecutionContext::test_default(),
1✔
866
        )
1✔
867
        .await?;
1✔
868

869
        let query_processor = initialized_operator.query_processor()?;
1✔
870

871
        let query_processor = query_processor.multi_line_string().unwrap();
1✔
872

873
        let query_rectangle = VectorQueryRectangle {
1✔
874
            spatial_bounds: BoundingBox2D::new(
1✔
875
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
876
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
877
            )
1✔
878
            .unwrap(),
1✔
879
            time_interval: TimeInterval::default(),
1✔
880
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
881
            attributes: ColumnSelection::all(),
1✔
882
        };
1✔
883
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
884

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

887
        let result = query
1✔
888
            .map(Result::unwrap)
1✔
889
            .collect::<Vec<MultiLineStringCollection>>()
1✔
890
            .await;
1✔
891

892
        assert_eq!(result.len(), 1);
1✔
893

894
        result[0]
1✔
895
            .geometries()
1✔
896
            .zip(expected.iter())
1✔
897
            .for_each(|(a, e)| {
1✔
898
                assert!(approx_eq!(
1✔
899
                    &MultiLineString,
900
                    &a.into(),
1✔
901
                    e,
1✔
902
                    epsilon = 0.00001
903
                ));
904
            });
1✔
905

906
        Ok(())
2✔
907
    }
1✔
908

909
    #[tokio::test]
910
    async fn multi_polygons() -> Result<()> {
1✔
911
        let polygons = MultiPolygonCollection::from_data(
1✔
912
            vec![
1✔
913
                MultiPolygon::new(vec![vec![vec![
1✔
914
                    MARBURG_EPSG_4326,
915
                    COLOGNE_EPSG_4326,
916
                    HAMBURG_EPSG_4326,
917
                    MARBURG_EPSG_4326,
918
                ]]])
919
                .unwrap(),
1✔
920
            ],
921
            vec![TimeInterval::new_unchecked(0, 1); 1],
1✔
922
            Default::default(),
1✔
923
            CacheHint::default(),
1✔
924
        )?;
×
925

926
        let expected = [MultiPolygon::new(vec![vec![vec![
1✔
927
            MARBURG_EPSG_900_913,
1✔
928
            COLOGNE_EPSG_900_913,
1✔
929
            HAMBURG_EPSG_900_913,
1✔
930
            MARBURG_EPSG_900_913,
1✔
931
        ]]])
1✔
932
        .unwrap()];
1✔
933

934
        let polygon_source = MockFeatureCollectionSource::single(polygons.clone()).boxed();
1✔
935

936
        let target_spatial_reference =
1✔
937
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 900_913);
1✔
938

939
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
940
            params: ReprojectionParams {
1✔
941
                target_spatial_reference,
1✔
942
            },
1✔
943
            sources: SingleRasterOrVectorSource {
1✔
944
                source: polygon_source.into(),
1✔
945
            },
1✔
946
        })
1✔
947
        .initialize(
1✔
948
            WorkflowOperatorPath::initialize_root(),
1✔
949
            &MockExecutionContext::test_default(),
1✔
950
        )
1✔
951
        .await?;
1✔
952

953
        let query_processor = initialized_operator.query_processor()?;
1✔
954

955
        let query_processor = query_processor.multi_polygon().unwrap();
1✔
956

957
        let query_rectangle = VectorQueryRectangle {
1✔
958
            spatial_bounds: BoundingBox2D::new(
1✔
959
                (COLOGNE_EPSG_4326.x, MARBURG_EPSG_4326.y).into(),
1✔
960
                (MARBURG_EPSG_4326.x, HAMBURG_EPSG_4326.y).into(),
1✔
961
            )
1✔
962
            .unwrap(),
1✔
963
            time_interval: TimeInterval::default(),
1✔
964
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
965
            attributes: ColumnSelection::all(),
1✔
966
        };
1✔
967
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
968

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

971
        let result = query
1✔
972
            .map(Result::unwrap)
1✔
973
            .collect::<Vec<MultiPolygonCollection>>()
1✔
974
            .await;
1✔
975

976
        assert_eq!(result.len(), 1);
1✔
977

978
        result[0]
1✔
979
            .geometries()
1✔
980
            .zip(expected.iter())
1✔
981
            .for_each(|(a, e)| {
1✔
982
                assert!(approx_eq!(&MultiPolygon, &a.into(), e, epsilon = 0.00001));
1✔
983
            });
1✔
984

985
        Ok(())
2✔
986
    }
1✔
987

988
    #[tokio::test]
989
    async fn raster_identity() -> Result<()> {
1✔
990
        let projection = SpatialReference::new(
1✔
991
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
992
            4326,
993
        );
994

995
        let data = vec![
1✔
996
            RasterTile2D {
1✔
997
                time: TimeInterval::new_unchecked(0, 5),
1✔
998
                tile_position: [-1, 0].into(),
1✔
999
                band: 0,
1✔
1000
                global_geo_transform: TestDefault::test_default(),
1✔
1001
                grid_array: Grid::new([2, 2].into(), vec![1, 2, 3, 4]).unwrap().into(),
1✔
1002
                properties: Default::default(),
1✔
1003
                cache_hint: CacheHint::default(),
1✔
1004
            },
1✔
1005
            RasterTile2D {
1✔
1006
                time: TimeInterval::new_unchecked(0, 5),
1✔
1007
                tile_position: [-1, 1].into(),
1✔
1008
                band: 0,
1✔
1009
                global_geo_transform: TestDefault::test_default(),
1✔
1010
                grid_array: Grid::new([2, 2].into(), vec![7, 8, 9, 10]).unwrap().into(),
1✔
1011
                properties: Default::default(),
1✔
1012
                cache_hint: CacheHint::default(),
1✔
1013
            },
1✔
1014
            RasterTile2D {
1✔
1015
                time: TimeInterval::new_unchecked(5, 10),
1✔
1016
                tile_position: [-1, 0].into(),
1✔
1017
                band: 0,
1✔
1018
                global_geo_transform: TestDefault::test_default(),
1✔
1019
                grid_array: Grid::new([2, 2].into(), vec![13, 14, 15, 16])
1✔
1020
                    .unwrap()
1✔
1021
                    .into(),
1✔
1022
                properties: Default::default(),
1✔
1023
                cache_hint: CacheHint::default(),
1✔
1024
            },
1✔
1025
            RasterTile2D {
1✔
1026
                time: TimeInterval::new_unchecked(5, 10),
1✔
1027
                tile_position: [-1, 1].into(),
1✔
1028
                band: 0,
1✔
1029
                global_geo_transform: TestDefault::test_default(),
1✔
1030
                grid_array: Grid::new([2, 2].into(), vec![19, 20, 21, 22])
1✔
1031
                    .unwrap()
1✔
1032
                    .into(),
1✔
1033
                properties: Default::default(),
1✔
1034
                cache_hint: CacheHint::default(),
1✔
1035
            },
1✔
1036
        ];
1037

1038
        let mrs1 = MockRasterSource {
1✔
1039
            params: MockRasterSourceParams {
1✔
1040
                data: data.clone(),
1✔
1041
                result_descriptor: RasterResultDescriptor {
1✔
1042
                    data_type: RasterDataType::U8,
1✔
1043
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1044
                    time: None,
1✔
1045
                    bbox: None,
1✔
1046
                    resolution: Some(SpatialResolution::one()),
1✔
1047
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1048
                },
1✔
1049
            },
1✔
1050
        }
1✔
1051
        .boxed();
1✔
1052

1053
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1054
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
1✔
1055
            // we need a smaller tile size
1✔
1056
            shape_array: [2, 2],
1✔
1057
        };
1✔
1058

1059
        let query_ctx = MockQueryContext::test_default();
1✔
1060

1061
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1062
            params: ReprojectionParams {
1✔
1063
                target_spatial_reference: projection, // This test will do a identity reprojection
1✔
1064
            },
1✔
1065
            sources: SingleRasterOrVectorSource {
1✔
1066
                source: mrs1.into(),
1✔
1067
            },
1✔
1068
        })
1✔
1069
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1070
        .await?;
1✔
1071

1072
        let qp = initialized_operator
1✔
1073
            .query_processor()
1✔
1074
            .unwrap()
1✔
1075
            .get_u8()
1✔
1076
            .unwrap();
1✔
1077

1078
        let query_rect = RasterQueryRectangle {
1✔
1079
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 1.).into(), (3., 0.).into()),
1✔
1080
            time_interval: TimeInterval::new_unchecked(0, 10),
1✔
1081
            spatial_resolution: SpatialResolution::one(),
1✔
1082
            attributes: BandSelection::first(),
1✔
1083
        };
1✔
1084

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

1087
        let res = a
1✔
1088
            .map(Result::unwrap)
1✔
1089
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1090
            .await;
1✔
1091
        assert!(data.tiles_equal_ignoring_cache_hint(&res));
1✔
1092

1093
        Ok(())
2✔
1094
    }
1✔
1095

1096
    #[tokio::test]
1097
    async fn raster_ndvi_3857() -> Result<()> {
1✔
1098
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1099
        let query_ctx = MockQueryContext::test_default();
1✔
1100
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1101
        exe_ctx.tiling_specification =
1✔
1102
            TilingSpecification::new((0.0, 0.0).into(), [450, 450].into());
1✔
1103

1104
        let output_shape: GridShape2D = [900, 1800].into();
1✔
1105
        let output_bounds =
1✔
1106
            SpatialPartition2D::new_unchecked((0., 20_000_000.).into(), (20_000_000., 0.).into());
1✔
1107
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001);
1✔
1108
        // 2014-01-01
1109

1110
        let gdal_op = GdalSource {
1✔
1111
            params: GdalSourceParameters { data: id.clone() },
1✔
1112
        }
1✔
1113
        .boxed();
1✔
1114

1115
        let projection = SpatialReference::new(
1✔
1116
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg,
1✔
1117
            3857,
1118
        );
1119

1120
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1121
            params: ReprojectionParams {
1✔
1122
                target_spatial_reference: projection,
1✔
1123
            },
1✔
1124
            sources: SingleRasterOrVectorSource {
1✔
1125
                source: gdal_op.into(),
1✔
1126
            },
1✔
1127
        })
1✔
1128
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1129
        .await?;
1✔
1130

1131
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
1132
        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✔
1133
        let spatial_resolution =
1✔
1134
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1135

1136
        let qp = initialized_operator
1✔
1137
            .query_processor()
1✔
1138
            .unwrap()
1✔
1139
            .get_u8()
1✔
1140
            .unwrap();
1✔
1141

1142
        let qs = qp
1✔
1143
            .raster_query(
1✔
1144
                RasterQueryRectangle {
1✔
1145
                    spatial_bounds: output_bounds,
1✔
1146
                    time_interval,
1✔
1147
                    spatial_resolution,
1✔
1148
                    attributes: BandSelection::first(),
1✔
1149
                },
1✔
1150
                &query_ctx,
1✔
1151
            )
1✔
1152
            .await
1✔
1153
            .unwrap();
1✔
1154

1155
        let res = qs
1✔
1156
            .map(Result::unwrap)
1✔
1157
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1158
            .await;
1✔
1159

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

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

1168
        assert_eq!(
1✔
1169
            include_bytes!(
1170
                "../../../test_data/raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01_tile-20.rst"
1171
            ) as &[u8],
1✔
1172
            res[8]
1✔
1173
                .clone()
1✔
1174
                .into_materialized_tile()
1✔
1175
                .grid_array
1✔
1176
                .inner_grid
1✔
1177
                .data
1✔
1178
                .as_slice()
1✔
1179
        );
1180

1181
        Ok(())
2✔
1182
    }
1✔
1183

1184
    #[test]
1185
    fn query_rewrite_4326_3857() {
1✔
1186
        let query = VectorQueryRectangle {
1✔
1187
            spatial_bounds: BoundingBox2D::new_unchecked((-180., -90.).into(), (180., 90.).into()),
1✔
1188
            time_interval: TimeInterval::default(),
1✔
1189
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1190
            attributes: ColumnSelection::all(),
1✔
1191
        };
1✔
1192

1193
        let expected = BoundingBox2D::new_unchecked(
1✔
1194
            (-20_037_508.342_789_244, -20_048_966.104_014_594).into(),
1✔
1195
            (20_037_508.342_789_244, 20_048_966.104_014_594).into(),
1✔
1196
        );
1197

1198
        let reprojected = reproject_query(
1✔
1199
            query,
1✔
1200
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857),
1✔
1201
            SpatialReference::epsg_4326(),
1✔
1202
            true,
1203
        )
1204
        .unwrap()
1✔
1205
        .unwrap();
1✔
1206

1207
        assert!(approx_eq!(
1✔
1208
            BoundingBox2D,
1209
            expected,
1✔
1210
            reprojected.spatial_bounds,
1✔
1211
            epsilon = 0.000_001
1212
        ));
1213
    }
1✔
1214

1215
    #[tokio::test]
1216
    async fn raster_ndvi_3857_to_4326() -> Result<()> {
1✔
1217
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1218
        let query_ctx = MockQueryContext::test_default();
1✔
1219

1220
        let m = GdalMetaDataRegular {
1✔
1221
            data_time: TimeInterval::new_unchecked(
1✔
1222
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1223
                TimeInstance::from_str("2014-07-01T00:00:00.000Z").unwrap(),
1✔
1224
            ),
1✔
1225
            step: TimeStep {
1✔
1226
                granularity: TimeGranularity::Months,
1✔
1227
                step: 1,
1✔
1228
            },
1✔
1229
            time_placeholders: hashmap! {
1✔
1230
                "%_START_TIME_%".to_string() => GdalSourceTimePlaceholder {
1✔
1231
                    format: DateTimeParseFormat::custom("%Y-%m-%d".to_string()),
1✔
1232
                    reference: TimeReference::Start,
1✔
1233
                },
1✔
1234
            },
1✔
1235
            params: GdalDatasetParameters {
1✔
1236
                file_path: test_data!(
1✔
1237
                    "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_%_START_TIME_%.TIFF"
1✔
1238
                )
1✔
1239
                .into(),
1✔
1240
                rasterband_channel: 1,
1✔
1241
                geo_transform: GdalDatasetGeoTransform {
1✔
1242
                    origin_coordinate: (-20_037_508.342_789_244, 19_971_868.880_408_563).into(),
1✔
1243
                    x_pixel_size: 14_052.950_258_048_739,
1✔
1244
                    y_pixel_size: -14_057.881_117_788_405,
1✔
1245
                },
1✔
1246
                width: 2851,
1✔
1247
                height: 2841,
1✔
1248
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1249
                no_data_value: Some(0.),
1✔
1250
                properties_mapping: None,
1✔
1251
                gdal_open_options: None,
1✔
1252
                gdal_config_options: None,
1✔
1253
                allow_alphaband_as_mask: true,
1✔
1254
                retry: None,
1✔
1255
            },
1✔
1256
            result_descriptor: RasterResultDescriptor {
1✔
1257
                data_type: RasterDataType::U8,
1✔
1258
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857)
1✔
1259
                    .into(),
1✔
1260
                time: None,
1✔
1261
                bbox: None,
1✔
1262
                resolution: None,
1✔
1263
                bands: RasterBandDescriptors::new_single_band(),
1✔
1264
            },
1✔
1265
            cache_ttl: CacheTtlSeconds::default(),
1✔
1266
        };
1✔
1267

1268
        let id: DataId = DatasetId::new().into();
1✔
1269
        let name = NamedData::with_system_name("ndvi");
1✔
1270
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1271

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

1274
        let output_bounds =
1✔
1275
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1276
        let time_interval = TimeInterval::new_unchecked(1_396_310_400_000, 1_396_310_400_000);
1✔
1277
        // 2014-04-01
1278

1279
        let gdal_op = GdalSource {
1✔
1280
            params: GdalSourceParameters { data: name },
1✔
1281
        }
1✔
1282
        .boxed();
1✔
1283

1284
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1285
            params: ReprojectionParams {
1✔
1286
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1287
            },
1✔
1288
            sources: SingleRasterOrVectorSource {
1✔
1289
                source: gdal_op.into(),
1✔
1290
            },
1✔
1291
        })
1✔
1292
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1293
        .await?;
1✔
1294

1295
        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✔
1296
        let y_query_resolution = output_bounds.size_y() / 240.; // *2 to account for the dataset aspect ratio 2:1
1✔
1297
        let spatial_resolution =
1✔
1298
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1299

1300
        let qp = initialized_operator
1✔
1301
            .query_processor()
1✔
1302
            .unwrap()
1✔
1303
            .get_u8()
1✔
1304
            .unwrap();
1✔
1305

1306
        let qs = qp
1✔
1307
            .raster_query(
1✔
1308
                QueryRectangle {
1✔
1309
                    spatial_bounds: output_bounds,
1✔
1310
                    time_interval,
1✔
1311
                    spatial_resolution,
1✔
1312
                    attributes: BandSelection::first(),
1✔
1313
                },
1✔
1314
                &query_ctx,
1✔
1315
            )
1✔
1316
            .await
1✔
1317
            .unwrap();
1✔
1318

1319
        let tiles = qs
1✔
1320
            .map(Result::unwrap)
1✔
1321
            .collect::<Vec<RasterTile2D<u8>>>()
1✔
1322
            .await;
1✔
1323

1324
        // the test must generate 8x4 tiles
1325
        assert_eq!(tiles.len(), 32);
1✔
1326

1327
        // none of the tiles should be empty
1328
        assert!(tiles.iter().all(|t| !t.is_empty()));
32✔
1329

1330
        Ok(())
2✔
1331
    }
1✔
1332

1333
    #[test]
1334
    fn source_resolution() {
1✔
1335
        let epsg_4326 = SpatialReference::epsg_4326();
1✔
1336
        let epsg_3857 = SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857);
1✔
1337

1338
        // use ndvi dataset that was reprojected using gdal as ground truth
1339
        let dataset_4326 = gdal_open_dataset(test_data!(
1✔
1340
            "raster/modis_ndvi/MOD13A2_M_NDVI_2014-04-01.TIFF"
1341
        ))
1342
        .unwrap();
1✔
1343
        let geotransform_4326 = dataset_4326.geo_transform().unwrap();
1✔
1344
        let res_4326 = SpatialResolution::new(geotransform_4326[1], -geotransform_4326[5]).unwrap();
1✔
1345

1346
        let dataset_3857 = gdal_open_dataset(test_data!(
1✔
1347
            "raster/modis_ndvi/projected_3857/MOD13A2_M_NDVI_2014-04-01.TIFF"
1348
        ))
1349
        .unwrap();
1✔
1350
        let geotransform_3857 = dataset_3857.geo_transform().unwrap();
1✔
1351
        let res_3857 = SpatialResolution::new(geotransform_3857[1], -geotransform_3857[5]).unwrap();
1✔
1352

1353
        // ndvi was projected from 4326 to 3857. The calculated source_resolution for getting the raster in 3857 with `res_3857`
1354
        // should thus roughly be like the original `res_4326`
1355
        let result_res = suggest_pixel_size_from_diag_cross_projected::<SpatialPartition2D>(
1✔
1356
            epsg_3857.area_of_use_projected().unwrap(),
1✔
1357
            epsg_4326.area_of_use_projected().unwrap(),
1✔
1358
            res_3857,
1✔
1359
        )
1360
        .unwrap();
1✔
1361
        assert!(1. - (result_res.x / res_4326.x).abs() < 0.02);
1✔
1362
        assert!(1. - (result_res.y / res_4326.y).abs() < 0.02);
1✔
1363
    }
1✔
1364

1365
    #[tokio::test]
1366
    async fn query_outside_projection_area_of_use_produces_empty_tiles() {
1✔
1367
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1368
        let query_ctx = MockQueryContext::test_default();
1✔
1369

1370
        let m = GdalMetaDataStatic {
1✔
1371
            time: Some(TimeInterval::default()),
1✔
1372
            params: GdalDatasetParameters {
1✔
1373
                file_path: PathBuf::new(),
1✔
1374
                rasterband_channel: 1,
1✔
1375
                geo_transform: GdalDatasetGeoTransform {
1✔
1376
                    origin_coordinate: (166_021.44, 9_329_005.188).into(),
1✔
1377
                    x_pixel_size: (534_994.66 - 166_021.444) / 100.,
1✔
1378
                    y_pixel_size: -9_329_005.18 / 100.,
1✔
1379
                },
1✔
1380
                width: 100,
1✔
1381
                height: 100,
1✔
1382
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1383
                no_data_value: Some(0.),
1✔
1384
                properties_mapping: None,
1✔
1385
                gdal_open_options: None,
1✔
1386
                gdal_config_options: None,
1✔
1387
                allow_alphaband_as_mask: true,
1✔
1388
                retry: None,
1✔
1389
            },
1✔
1390
            result_descriptor: RasterResultDescriptor {
1✔
1391
                data_type: RasterDataType::U8,
1✔
1392
                spatial_reference: SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636)
1✔
1393
                    .into(),
1✔
1394
                time: None,
1✔
1395
                bbox: None,
1✔
1396
                resolution: None,
1✔
1397
                bands: RasterBandDescriptors::new_single_band(),
1✔
1398
            },
1✔
1399
            cache_ttl: CacheTtlSeconds::default(),
1✔
1400
        };
1✔
1401

1402
        let id: DataId = DatasetId::new().into();
1✔
1403
        let name = NamedData::with_system_name("ndvi");
1✔
1404
        exe_ctx.add_meta_data(id.clone(), name.clone(), Box::new(m));
1✔
1405

1406
        exe_ctx.tiling_specification =
1✔
1407
            TilingSpecification::new((0.0, 0.0).into(), [600, 600].into());
1✔
1408

1409
        let output_shape: GridShape2D = [1000, 1000].into();
1✔
1410
        let output_bounds =
1✔
1411
            SpatialPartition2D::new_unchecked((-180., 0.).into(), (180., -90.).into());
1✔
1412
        let time_interval = TimeInterval::new_instant(1_388_534_400_000).unwrap(); // 2014-01-01
1✔
1413

1414
        let gdal_op = GdalSource {
1✔
1415
            params: GdalSourceParameters { data: name },
1✔
1416
        }
1✔
1417
        .boxed();
1✔
1418

1419
        let initialized_operator = RasterOperator::boxed(Reprojection {
1✔
1420
            params: ReprojectionParams {
1✔
1421
                target_spatial_reference: SpatialReference::epsg_4326(),
1✔
1422
            },
1✔
1423
            sources: SingleRasterOrVectorSource {
1✔
1424
                source: gdal_op.into(),
1✔
1425
            },
1✔
1426
        })
1✔
1427
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1428
        .await
1✔
1429
        .unwrap();
1✔
1430

1431
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
1✔
1432
        let y_query_resolution = output_bounds.size_y() / (output_shape.axis_size_y()) as f64;
1✔
1433
        let spatial_resolution =
1✔
1434
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1✔
1435

1436
        let qp = initialized_operator
1✔
1437
            .query_processor()
1✔
1438
            .unwrap()
1✔
1439
            .get_u8()
1✔
1440
            .unwrap();
1✔
1441

1442
        let result = qp
1✔
1443
            .raster_query(
1✔
1444
                QueryRectangle {
1✔
1445
                    spatial_bounds: output_bounds,
1✔
1446
                    time_interval,
1✔
1447
                    spatial_resolution,
1✔
1448
                    attributes: BandSelection::first(),
1✔
1449
                },
1✔
1450
                &query_ctx,
1✔
1451
            )
1✔
1452
            .await
1✔
1453
            .unwrap()
1✔
1454
            .map(Result::unwrap)
1✔
1455
            .collect::<Vec<_>>()
1✔
1456
            .await;
1✔
1457

1458
        assert_eq!(result.len(), 4);
1✔
1459

1460
        for r in result {
5✔
1461
            assert!(r.is_empty());
4✔
1462
        }
1✔
1463
    }
1✔
1464

1465
    #[tokio::test]
1466
    async fn points_from_wgs84_to_utm36n() {
1✔
1467
        let exe_ctx = MockExecutionContext::test_default();
1✔
1468
        let query_ctx = MockQueryContext::test_default();
1✔
1469

1470
        let point_source = MockFeatureCollectionSource::single(
1✔
1471
            MultiPointCollection::from_data(
1✔
1472
                MultiPoint::many(vec![
1✔
1473
                    vec![(30.0, 0.0)], // lower left of utm36n area of use
1✔
1474
                    vec![(36.0, 84.0)],
1✔
1475
                    vec![(33.0, 42.0)], // upper right of utm36n area of use
1✔
1476
                ])
1477
                .unwrap(),
1✔
1478
                vec![TimeInterval::default(); 3],
1✔
1479
                HashMap::default(),
1✔
1480
                CacheHint::default(),
1✔
1481
            )
1482
            .unwrap(),
1✔
1483
        )
1484
        .boxed();
1✔
1485

1486
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1487
            params: ReprojectionParams {
1✔
1488
                target_spatial_reference: SpatialReference::new(
1✔
1489
                    SpatialReferenceAuthority::Epsg,
1✔
1490
                    32636, // utm36n
1✔
1491
                ),
1✔
1492
            },
1✔
1493
            sources: SingleRasterOrVectorSource {
1✔
1494
                source: point_source.into(),
1✔
1495
            },
1✔
1496
        })
1✔
1497
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1498
        .await
1✔
1499
        .unwrap();
1✔
1500

1501
        let qp = initialized_operator
1✔
1502
            .query_processor()
1✔
1503
            .unwrap()
1✔
1504
            .multi_point()
1✔
1505
            .unwrap();
1✔
1506

1507
        let spatial_bounds = BoundingBox2D::new(
1✔
1508
            (166_021.44, 0.00).into(), // lower left of projected utm36n area of use
1✔
1509
            (534_994.666_6, 9_329_005.18).into(), // upper right of projected utm36n area of use
1✔
1510
        )
1511
        .unwrap();
1✔
1512

1513
        let qs = qp
1✔
1514
            .vector_query(
1✔
1515
                QueryRectangle {
1✔
1516
                    spatial_bounds,
1✔
1517
                    time_interval: TimeInterval::default(),
1✔
1518
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1519
                    attributes: ColumnSelection::all(),
1✔
1520
                },
1✔
1521
                &query_ctx,
1✔
1522
            )
1✔
1523
            .await
1✔
1524
            .unwrap();
1✔
1525

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

1528
        assert_eq!(points.len(), 1);
1✔
1529

1530
        let points = &points[0];
1✔
1531

1532
        assert_eq!(
1✔
1533
            points.coordinates(),
1✔
1534
            &[
1✔
1535
                (166_021.443_080_538_42, 0.0).into(),
1✔
1536
                (534_994.655_061_136_1, 9_329_005.182_447_437).into(),
1✔
1537
                (499_999.999_999_999_5, 4_649_776.224_819_178).into()
1✔
1538
            ]
1✔
1539
        );
1✔
1540
    }
1✔
1541

1542
    #[tokio::test]
1543
    async fn points_from_utm36n_to_wgs84() {
1✔
1544
        let exe_ctx = MockExecutionContext::test_default();
1✔
1545
        let query_ctx = MockQueryContext::test_default();
1✔
1546

1547
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1548
            vec![
1✔
1549
                MultiPointCollection::from_data(
1✔
1550
                    MultiPoint::many(vec![
1✔
1551
                        vec![(166_021.443_080_538_42, 0.0)],
1✔
1552
                        vec![(534_994.655_061_136_1, 9_329_005.182_447_437)],
1✔
1553
                        vec![(499_999.999_999_999_5, 4_649_776.224_819_178)],
1✔
1554
                    ])
1555
                    .unwrap(),
1✔
1556
                    vec![TimeInterval::default(); 3],
1✔
1557
                    HashMap::default(),
1✔
1558
                    CacheHint::default(),
1✔
1559
                )
1560
                .unwrap(),
1✔
1561
            ],
1562
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1563
        )
1564
        .boxed();
1✔
1565

1566
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1567
            params: ReprojectionParams {
1✔
1568
                target_spatial_reference: SpatialReference::new(
1✔
1569
                    SpatialReferenceAuthority::Epsg,
1✔
1570
                    4326, // utm36n
1✔
1571
                ),
1✔
1572
            },
1✔
1573
            sources: SingleRasterOrVectorSource {
1✔
1574
                source: point_source.into(),
1✔
1575
            },
1✔
1576
        })
1✔
1577
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1578
        .await
1✔
1579
        .unwrap();
1✔
1580

1581
        let qp = initialized_operator
1✔
1582
            .query_processor()
1✔
1583
            .unwrap()
1✔
1584
            .multi_point()
1✔
1585
            .unwrap();
1✔
1586

1587
        let spatial_bounds = BoundingBox2D::new(
1✔
1588
            (30.0, 0.0).into(),  // lower left of utm36n area of use
1✔
1589
            (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1590
        )
1591
        .unwrap();
1✔
1592

1593
        let qs = qp
1✔
1594
            .vector_query(
1✔
1595
                QueryRectangle {
1✔
1596
                    spatial_bounds,
1✔
1597
                    time_interval: TimeInterval::default(),
1✔
1598
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1599
                    attributes: ColumnSelection::all(),
1✔
1600
                },
1✔
1601
                &query_ctx,
1✔
1602
            )
1✔
1603
            .await
1✔
1604
            .unwrap();
1✔
1605

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

1608
        assert_eq!(points.len(), 1);
1✔
1609

1610
        let points = &points[0];
1✔
1611

1612
        assert!(approx_eq!(
1✔
1613
            &[Coordinate2D],
1✔
1614
            points.coordinates(),
1✔
1615
            &[
1✔
1616
                (30.0, 0.0).into(), // lower left of utm36n area of use
1✔
1617
                (36.0, 84.0).into(),
1✔
1618
                (33.0, 42.0).into(), // upper right of utm36n area of use
1✔
1619
            ]
1✔
1620
        ));
1✔
1621
    }
1✔
1622

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

1627
        // 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
1628

1629
        let exe_ctx = MockExecutionContext::test_default();
1✔
1630
        let query_ctx = MockQueryContext::test_default();
1✔
1631

1632
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
1633
            vec![
1✔
1634
                MultiPointCollection::from_data(
1✔
1635
                    MultiPoint::many(vec![
1✔
1636
                        vec![(758_565., 4_928_353.)], // (12.25, 44,46)
1✔
1637
                    ])
1638
                    .unwrap(),
1✔
1639
                    vec![TimeInterval::default(); 1],
1✔
1640
                    HashMap::default(),
1✔
1641
                    CacheHint::default(),
1✔
1642
                )
1643
                .unwrap(),
1✔
1644
            ],
1645
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 32636), //utm36n
1✔
1646
        )
1647
        .boxed();
1✔
1648

1649
        let initialized_operator = VectorOperator::boxed(Reprojection {
1✔
1650
            params: ReprojectionParams {
1✔
1651
                target_spatial_reference: SpatialReference::new(
1✔
1652
                    SpatialReferenceAuthority::Epsg,
1✔
1653
                    4326, // utm36n
1✔
1654
                ),
1✔
1655
            },
1✔
1656
            sources: SingleRasterOrVectorSource {
1✔
1657
                source: point_source.into(),
1✔
1658
            },
1✔
1659
        })
1✔
1660
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1661
        .await
1✔
1662
        .unwrap();
1✔
1663

1664
        let qp = initialized_operator
1✔
1665
            .query_processor()
1✔
1666
            .unwrap()
1✔
1667
            .multi_point()
1✔
1668
            .unwrap();
1✔
1669

1670
        let spatial_bounds = BoundingBox2D::new(
1✔
1671
            (10.0, 0.0).into(),  // -20 x values left of lower left of utm36n area of use
1✔
1672
            (13.0, 42.0).into(), // -20 x values left of upper right of utm36n area of use
1✔
1673
        )
1674
        .unwrap();
1✔
1675

1676
        let qs = qp
1✔
1677
            .vector_query(
1✔
1678
                QueryRectangle {
1✔
1679
                    spatial_bounds,
1✔
1680
                    time_interval: TimeInterval::default(),
1✔
1681
                    spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1682
                    attributes: ColumnSelection::all(),
1✔
1683
                },
1✔
1684
                &query_ctx,
1✔
1685
            )
1✔
1686
            .await
1✔
1687
            .unwrap();
1✔
1688

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

1691
        assert_eq!(points.len(), 1);
1✔
1692

1693
        //let points = &points[0];
1694

1695
        //assert!(geoengine_datatypes::collections::FeatureCollectionInfos::is_empty(points));
1696
        //assert!(points.coordinates().is_empty());
1697
    }
1✔
1698

1699
    #[test]
1700
    fn it_derives_raster_result_descriptor() {
1✔
1701
        let in_proj = SpatialReference::epsg_4326();
1✔
1702
        let out_proj = SpatialReference::from_str("EPSG:3857").unwrap();
1✔
1703
        let bbox = Some(SpatialPartition2D::new_unchecked(
1✔
1704
            (-180., 90.).into(),
1✔
1705
            (180., -90.).into(),
1✔
1706
        ));
1✔
1707

1708
        let resolution = Some(SpatialResolution::new_unchecked(0.1, 0.1));
1✔
1709

1710
        let (in_bounds, out_bounds, out_res) =
1✔
1711
            InitializedRasterReprojection::derive_raster_in_bounds_out_bounds_out_res(
1✔
1712
                in_proj, out_proj, resolution, bbox,
1✔
1713
            )
1✔
1714
            .unwrap();
1✔
1715

1716
        assert_eq!(
1✔
1717
            in_bounds.unwrap(),
1✔
1718
            SpatialPartition2D::new_unchecked((-180., 85.06).into(), (180., -85.06).into(),)
1✔
1719
        );
1720

1721
        assert_eq!(
1✔
1722
            out_bounds.unwrap(),
1✔
1723
            out_proj
1✔
1724
                .area_of_use_projected::<SpatialPartition2D>()
1✔
1725
                .unwrap()
1✔
1726
        );
1727

1728
        // TODO: y resolution should be double the x resolution, but currently we only compute a uniform resolution
1729
        assert_eq!(
1✔
1730
            out_res.unwrap(),
1✔
1731
            SpatialResolution::new_unchecked(14_237.781_884_528_267, 14_237.781_884_528_267),
1✔
1732
        );
1733
    }
1✔
1734
}
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