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

geo-engine / geoengine / 11910714914

19 Nov 2024 10:06AM UTC coverage: 90.445% (-0.2%) from 90.687%
11910714914

push

github

web-flow
Merge pull request #994 from geo-engine/workspace-dependencies

use workspace dependencies, update toolchain, use global lock in expression

9 of 11 new or added lines in 6 files covered. (81.82%)

375 existing lines in 75 files now uncovered.

132867 of 146904 relevant lines covered (90.44%)

54798.11 hits per line

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

98.68
/operators/src/source/gdal_source/loading_info.rs
1
use super::{GdalDatasetParameters, GdalSourceTimePlaceholder};
2
use crate::{
3
    engine::{MetaData, RasterResultDescriptor},
4
    error::Error,
5
    util::Result,
6
};
7
use async_trait::async_trait;
8
use geoengine_datatypes::primitives::{
9
    CacheTtlSeconds, RasterQueryRectangle, TimeInstance, TimeInterval, TimeStep, TimeStepIter,
10
};
11
use postgres_types::{FromSql, ToSql};
12
use serde::{Deserialize, Serialize};
13
use std::collections::HashMap;
14

15
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, FromSql, ToSql)]
872✔
16
#[serde(rename_all = "camelCase")]
17
pub struct GdalMetaDataStatic {
18
    pub time: Option<TimeInterval>,
19
    pub params: GdalDatasetParameters,
20
    pub result_descriptor: RasterResultDescriptor,
21
    #[serde(default)]
22
    pub cache_ttl: CacheTtlSeconds,
23
}
24

25
#[async_trait]
26
impl MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
27
    for GdalMetaDataStatic
28
{
29
    async fn loading_info(&self, query: RasterQueryRectangle) -> Result<GdalLoadingInfo> {
13✔
30
        let valid = self.time.unwrap_or_default();
13✔
31

32
        let parts = if valid.intersects(&query.time_interval) {
13✔
33
            vec![GdalLoadingInfoTemporalSlice {
13✔
34
                time: valid,
13✔
35
                params: Some(self.params.clone()),
13✔
36
                cache_ttl: self.cache_ttl,
13✔
37
            }]
13✔
38
        } else {
UNCOV
39
            vec![]
×
40
        };
41

42
        let known_time_before = if query.time_interval.start() < valid.start() {
13✔
UNCOV
43
            TimeInstance::MIN
×
44
        } else if query.time_interval.start() < valid.end() {
13✔
45
            valid.start()
11✔
46
        } else {
47
            valid.end()
2✔
48
        };
49

50
        let known_time_after = if query.time_interval.end() <= valid.start() {
13✔
51
            valid.start()
2✔
52
        } else if query.time_interval.end() <= valid.end() {
11✔
53
            valid.end()
11✔
54
        } else {
UNCOV
55
            TimeInstance::MAX
×
56
        };
57

58
        Ok(GdalLoadingInfo::new(
13✔
59
            GdalLoadingInfoTemporalSliceIterator::Static {
13✔
60
                parts: parts.into_iter(),
13✔
61
            },
13✔
62
            known_time_before,
13✔
63
            known_time_after,
13✔
64
        ))
13✔
65
    }
26✔
66

67
    async fn result_descriptor(&self) -> Result<RasterResultDescriptor> {
21✔
68
        Ok(self.result_descriptor.clone())
21✔
69
    }
42✔
70

71
    fn box_clone(
12✔
72
        &self,
12✔
73
    ) -> Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> {
12✔
74
        Box::new(self.clone())
12✔
75
    }
12✔
76
}
77

78
/// Meta data for a regular time series that begins and ends at the given `data_time` interval with multiple gdal data
79
/// sets `step` time apart. The `time_placeholders` in the file path of the dataset are replaced with the
80
/// specified time `reference` in specified time `format`. Inside the `data_time` the gdal source will load the data
81
/// from the files and outside it will create nodata.
82
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone)]
7✔
83
#[serde(rename_all = "camelCase")]
84
pub struct GdalMetaDataRegular {
85
    pub result_descriptor: RasterResultDescriptor,
86
    pub params: GdalDatasetParameters,
87
    pub time_placeholders: HashMap<String, GdalSourceTimePlaceholder>,
88
    pub data_time: TimeInterval,
89
    pub step: TimeStep,
90
    #[serde(default)]
91
    pub cache_ttl: CacheTtlSeconds,
92
}
93

94
#[async_trait]
95
impl MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
96
    for GdalMetaDataRegular
97
{
98
    async fn loading_info(&self, query: RasterQueryRectangle) -> Result<GdalLoadingInfo> {
102✔
99
        let data_time = self.data_time;
102✔
100
        let step = self.step;
102✔
101

102✔
102
        let mut known_time_start: Option<TimeInstance> = None;
102✔
103
        let mut known_time_end: Option<TimeInstance> = None;
102✔
104

102✔
105
        // TODO: reverse one step?
102✔
106
        TimeStepIter::new_with_interval(data_time, step)?
102✔
107
            .into_intervals(step, data_time.end())
102✔
108
            .for_each(|time_interval| {
594✔
109
                if time_interval.start() <= query.time_interval.start() {
594✔
110
                    let t = if time_interval.end() > query.time_interval.start() {
227✔
111
                        time_interval.start()
95✔
112
                    } else {
113
                        time_interval.end()
132✔
114
                    };
115
                    known_time_start = known_time_start.map(|old| old.max(t)).or(Some(t));
227✔
116
                }
367✔
117

118
                if time_interval.end() >= query.time_interval.end() {
594✔
119
                    let t = if time_interval.start() < query.time_interval.end() {
473✔
120
                        time_interval.end()
63✔
121
                    } else {
122
                        time_interval.start()
410✔
123
                    };
124
                    known_time_end = known_time_end.map(|old| old.min(t)).or(Some(t));
473✔
125
                }
473✔
126
            });
594✔
127

102✔
128
        // if we found no time bound we can assume that there is no data
102✔
129
        let known_time_before = known_time_start.unwrap_or(TimeInstance::MIN);
102✔
130
        let known_time_after = known_time_end.unwrap_or(TimeInstance::MAX);
102✔
131

102✔
132
        Ok(GdalLoadingInfo::new(
102✔
133
            GdalLoadingInfoTemporalSliceIterator::Dynamic(DynamicGdalLoadingInfoPartIterator::new(
102✔
134
                self.params.clone(),
102✔
135
                self.time_placeholders.clone(),
102✔
136
                self.step,
102✔
137
                query.time_interval,
102✔
138
                self.data_time,
102✔
139
                self.cache_ttl,
102✔
140
            )?),
102✔
141
            known_time_before,
102✔
142
            known_time_after,
102✔
143
        ))
144
    }
204✔
145

146
    async fn result_descriptor(&self) -> Result<RasterResultDescriptor> {
146✔
147
        Ok(self.result_descriptor.clone())
146✔
148
    }
292✔
149

150
    fn box_clone(
61✔
151
        &self,
61✔
152
    ) -> Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> {
61✔
153
        Box::new(self.clone())
61✔
154
    }
61✔
155
}
156

157
/// Meta data for 4D `NetCDF` CF datasets
158
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone)]
×
159
#[serde(rename_all = "camelCase")]
160
pub struct GdalMetadataNetCdfCf {
161
    pub result_descriptor: RasterResultDescriptor,
162
    pub params: GdalDatasetParameters,
163
    pub start: TimeInstance,
164
    /// We use the end to specify the last, non-inclusive valid time point.
165
    /// Queries behind this point return no data.
166
    /// TODO: Alternatively, we could think about using the number of possible time steps in the future.
167
    pub end: TimeInstance,
168
    pub step: TimeStep,
169
    /// A band offset specifies the first band index to use for the first point in time.
170
    /// All other time steps are added to this offset.
171
    pub band_offset: usize,
172
    #[serde(default)]
173
    pub cache_ttl: CacheTtlSeconds,
174
}
175

176
#[async_trait]
177
// TODO: handle queries before and after valid time like in `GdalMetaDataRegular`
178
impl MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
179
    for GdalMetadataNetCdfCf
180
{
181
    async fn loading_info(&self, query: RasterQueryRectangle) -> Result<GdalLoadingInfo> {
3✔
182
        // special case: single step
183
        if self.start == self.end || self.step.step == 0 {
3✔
184
            let time = TimeInterval::new(self.start, self.end)?;
2✔
185

186
            let mut params = self.params.clone();
2✔
187
            params.rasterband_channel = 1 /* GDAL starts at 1 */ + self.band_offset;
2✔
188

2✔
189
            return GdalMetaDataStatic {
2✔
190
                time: Some(time),
2✔
191
                params,
2✔
192
                result_descriptor: self.result_descriptor.clone(),
2✔
193
                cache_ttl: self.cache_ttl,
2✔
194
            }
2✔
195
            .loading_info(query)
2✔
UNCOV
196
            .await;
×
197
        }
1✔
198

199
        let snapped_start = self
1✔
200
            .step
1✔
201
            .snap_relative(self.start, query.time_interval.start())?;
1✔
202

203
        let snapped_interval =
1✔
204
            TimeInterval::new_unchecked(snapped_start, query.time_interval.end()); // TODO: snap end?
1✔
205

206
        let time_iterator = TimeStepIter::new_with_interval(snapped_interval, self.step)?;
1✔
207

208
        Ok(GdalLoadingInfo::new_no_known_time_bounds(
1✔
209
            GdalLoadingInfoTemporalSliceIterator::NetCdfCf(
1✔
210
                NetCdfCfGdalLoadingInfoPartIterator::new(
1✔
211
                    time_iterator,
1✔
212
                    self.params.clone(),
1✔
213
                    self.step,
1✔
214
                    self.start,
1✔
215
                    self.end,
1✔
216
                    self.band_offset,
1✔
217
                    self.cache_ttl,
1✔
218
                ),
1✔
219
            ),
1✔
220
        ))
1✔
221
    }
6✔
222

223
    async fn result_descriptor(&self) -> Result<RasterResultDescriptor> {
×
224
        Ok(self.result_descriptor.clone())
×
225
    }
×
226

227
    fn box_clone(
×
228
        &self,
×
229
    ) -> Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> {
×
230
        Box::new(self.clone())
×
231
    }
×
232
}
233

234
// TODO: custom deserializer that checks that that params are sorted and do not overlap
235
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, FromSql, ToSql)]
697✔
236
#[serde(rename_all = "camelCase")]
237
pub struct GdalMetaDataList {
238
    pub result_descriptor: RasterResultDescriptor,
239
    pub params: Vec<GdalLoadingInfoTemporalSlice>,
240
}
241

242
#[async_trait]
243
impl MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle> for GdalMetaDataList {
244
    async fn loading_info(&self, query: RasterQueryRectangle) -> Result<GdalLoadingInfo> {
11✔
245
        let mut known_time_start: Option<TimeInstance> = None;
11✔
246
        let mut known_time_end: Option<TimeInstance> = None;
11✔
247

11✔
248
        let data = self
11✔
249
            .params
11✔
250
            .iter()
11✔
251
            .inspect(|m| {
31✔
252
                let time_interval = m.time;
31✔
253

31✔
254
                if time_interval.start() <= query.time_interval.start() {
31✔
255
                    let t = if time_interval.end() > query.time_interval.start() {
17✔
256
                        time_interval.start()
4✔
257
                    } else {
258
                        time_interval.end()
13✔
259
                    };
260
                    known_time_start = known_time_start.map(|old| old.max(t)).or(Some(t));
17✔
261
                }
17✔
262

263
                if time_interval.end() >= query.time_interval.end() {
31✔
264
                    let t = if time_interval.start() < query.time_interval.end() {
11✔
265
                        time_interval.end()
4✔
266
                    } else {
267
                        time_interval.start()
7✔
268
                    };
269
                    known_time_end = known_time_end.map(|old| old.min(t)).or(Some(t));
11✔
270
                }
20✔
271
            })
31✔
272
            .filter(|m| m.time.intersects(&query.time_interval))
31✔
273
            .cloned()
11✔
274
            .collect::<Vec<_>>();
11✔
275

11✔
276
        // if we found no time bound we can assume that there is no data
11✔
277
        let known_time_before = known_time_start.unwrap_or(TimeInstance::MIN);
11✔
278
        let known_time_after = known_time_end.unwrap_or(TimeInstance::MAX);
11✔
279

11✔
280
        Ok(GdalLoadingInfo::new(
11✔
281
            GdalLoadingInfoTemporalSliceIterator::Static {
11✔
282
                parts: data.into_iter(),
11✔
283
            },
11✔
284
            known_time_before,
11✔
285
            known_time_after,
11✔
286
        ))
11✔
287
    }
22✔
288

289
    async fn result_descriptor(&self) -> Result<RasterResultDescriptor> {
14✔
290
        Ok(self.result_descriptor.clone())
14✔
291
    }
28✔
292

293
    fn box_clone(
3✔
294
        &self,
3✔
295
    ) -> Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> {
3✔
296
        Box::new(self.clone())
3✔
297
    }
3✔
298
}
299

300
#[derive(Debug, Clone)]
301
/// An iterator for gdal loading infos based on time placeholders that generates
302
/// a new loading info for each time step within `data_time` and an empty loading info
303
/// for the time before and after `date_time`.
304
pub struct DynamicGdalLoadingInfoPartIterator {
305
    time_step_iter: TimeStepIter,
306
    params: GdalDatasetParameters,
307
    time_placeholders: HashMap<String, GdalSourceTimePlaceholder>,
308
    step: TimeStep,
309
    query_time: TimeInterval,
310
    data_time: TimeInterval,
311
    state: DynamicGdalLoadingInfoPartIteratorState,
312
    cache_ttl: CacheTtlSeconds,
313
}
314

315
#[derive(Debug, Clone)]
316
enum DynamicGdalLoadingInfoPartIteratorState {
317
    BeforeDataTime,
318
    WithinDataTime,
319
    AfterDataTime,
320
    Finished,
321
}
322

323
impl DynamicGdalLoadingInfoPartIterator {
324
    fn new(
102✔
325
        params: GdalDatasetParameters,
102✔
326
        time_placeholders: HashMap<String, GdalSourceTimePlaceholder>,
102✔
327
        step: TimeStep,
102✔
328
        query_time: TimeInterval,
102✔
329
        data_time: TimeInterval,
102✔
330
        cache_ttl: CacheTtlSeconds,
102✔
331
    ) -> Result<Self> {
102✔
332
        // TODO: maybe fail on deserialization
102✔
333
        if time_placeholders.is_empty()
102✔
334
            || time_placeholders.keys().any(String::is_empty)
102✔
335
            || time_placeholders
102✔
336
                .values()
102✔
337
                .any(|value| value.format.is_empty())
102✔
338
        {
339
            return Err(Error::DynamicGdalSourceSpecHasEmptyTimePlaceholders);
×
340
        }
102✔
341

342
        // depending on whether the query starts before, within or after the data, the time step iterator has to begin at a different point in time
343
        // because it only produces time steps within the data time. Before and after are handled separately.
344
        let (snapped_start, state) = if query_time.start() < data_time.start() {
102✔
345
            (
5✔
346
                data_time.start(),
5✔
347
                DynamicGdalLoadingInfoPartIteratorState::BeforeDataTime,
5✔
348
            )
5✔
349
        } else if query_time.start() < data_time.end() {
97✔
350
            (
351
                step.snap_relative(data_time.start(), query_time.start())?,
95✔
352
                DynamicGdalLoadingInfoPartIteratorState::WithinDataTime,
95✔
353
            )
354
        } else {
355
            (
2✔
356
                data_time.end(),
2✔
357
                DynamicGdalLoadingInfoPartIteratorState::AfterDataTime,
2✔
358
            )
2✔
359
        };
360

361
        // cap at end of data
362
        let mut end = query_time.end().min(data_time.end());
102✔
363

364
        // snap the end time to the _next_ step within in the `data_time`
365
        let snapped_end = step.snap_relative(data_time.start(), end)?;
102✔
366
        if snapped_end < end {
102✔
367
            end = (snapped_end + step)?;
20✔
368
        }
82✔
369

370
        // ensure start <= end
371
        end = end.max(snapped_start);
102✔
372

102✔
373
        let snapped_interval = TimeInterval::new_unchecked(snapped_start, end);
102✔
374

375
        let time_step_iter = TimeStepIter::new_with_interval(snapped_interval, step)?;
102✔
376

377
        Ok(Self {
102✔
378
            time_step_iter,
102✔
379
            params,
102✔
380
            time_placeholders,
102✔
381
            step,
102✔
382
            query_time,
102✔
383
            data_time,
102✔
384
            state,
102✔
385
            cache_ttl,
102✔
386
        })
102✔
387
    }
102✔
388
}
389

390
impl Iterator for DynamicGdalLoadingInfoPartIterator {
391
    type Item = Result<GdalLoadingInfoTemporalSlice>;
392

393
    fn next(&mut self) -> Option<Self::Item> {
222✔
394
        match self.state {
222✔
395
            DynamicGdalLoadingInfoPartIteratorState::BeforeDataTime => {
396
                if self.query_time.end() > self.data_time.start() {
5✔
397
                    self.state = DynamicGdalLoadingInfoPartIteratorState::WithinDataTime;
2✔
398
                } else {
3✔
399
                    self.state = DynamicGdalLoadingInfoPartIteratorState::Finished;
3✔
400
                }
3✔
401
                Some(Ok(GdalLoadingInfoTemporalSlice {
5✔
402
                    time: TimeInterval::new_unchecked(TimeInstance::MIN, self.data_time.start()),
5✔
403
                    params: None,
5✔
404
                    cache_ttl: self.cache_ttl,
5✔
405
                }))
5✔
406
            }
407
            DynamicGdalLoadingInfoPartIteratorState::WithinDataTime => {
408
                if let Some(t) = self.time_step_iter.next() {
209✔
409
                    let t2 = t + self.step;
114✔
410
                    let t2 = t2.unwrap_or_else(|_| self.data_time.end());
114✔
411
                    let time_interval = TimeInterval::new_unchecked(t, t2);
114✔
412

114✔
413
                    let loading_info_part = self
114✔
414
                        .params
114✔
415
                        .replace_time_placeholders(&self.time_placeholders, time_interval)
114✔
416
                        .map(|loading_info_part_params| GdalLoadingInfoTemporalSlice {
114✔
417
                            time: time_interval,
114✔
418
                            params: Some(loading_info_part_params),
114✔
419
                            cache_ttl: self.cache_ttl,
114✔
420
                        })
114✔
421
                        .map_err(Into::into);
114✔
422

114✔
423
                    Some(loading_info_part)
114✔
424
                } else {
425
                    self.state = DynamicGdalLoadingInfoPartIteratorState::Finished;
95✔
426

95✔
427
                    if self.query_time.end() > self.data_time.end() {
95✔
428
                        Some(Ok(GdalLoadingInfoTemporalSlice {
1✔
429
                            time: TimeInterval::new_unchecked(
1✔
430
                                self.data_time.end(),
1✔
431
                                TimeInstance::MAX,
1✔
432
                            ),
1✔
433
                            params: None,
1✔
434
                            cache_ttl: self.cache_ttl,
1✔
435
                        }))
1✔
436
                    } else {
437
                        None
94✔
438
                    }
439
                }
440
            }
441
            DynamicGdalLoadingInfoPartIteratorState::AfterDataTime => {
442
                self.state = DynamicGdalLoadingInfoPartIteratorState::Finished;
2✔
443

2✔
444
                Some(Ok(GdalLoadingInfoTemporalSlice {
2✔
445
                    time: TimeInterval::new_unchecked(self.data_time.end(), TimeInstance::MAX),
2✔
446
                    params: None,
2✔
447
                    cache_ttl: self.cache_ttl,
2✔
448
                }))
2✔
449
            }
450
            DynamicGdalLoadingInfoPartIteratorState::Finished => None,
6✔
451
        }
452
    }
222✔
453
}
454

455
#[derive(Debug, Clone)]
456
pub struct NetCdfCfGdalLoadingInfoPartIterator {
457
    time_step_iter: TimeStepIter,
458
    params: GdalDatasetParameters,
459
    step: TimeStep,
460
    dataset_time_start: TimeInstance,
461
    max_t2: TimeInstance,
462
    band_offset: usize,
463
    cache_ttl: CacheTtlSeconds,
464
}
465

466
impl NetCdfCfGdalLoadingInfoPartIterator {
467
    fn new(
1✔
468
        time_step_iter: TimeStepIter,
1✔
469
        params: GdalDatasetParameters,
1✔
470
        step: TimeStep,
1✔
471
        dataset_time_start: TimeInstance,
1✔
472
        max_t2: TimeInstance,
1✔
473
        band_offset: usize,
1✔
474
        cache_ttl: CacheTtlSeconds,
1✔
475
    ) -> Self {
1✔
476
        Self {
1✔
477
            time_step_iter,
1✔
478
            params,
1✔
479
            step,
1✔
480
            dataset_time_start,
1✔
481
            max_t2,
1✔
482
            band_offset,
1✔
483
            cache_ttl,
1✔
484
        }
1✔
485
    }
1✔
486
}
487

488
impl Iterator for NetCdfCfGdalLoadingInfoPartIterator {
489
    type Item = Result<GdalLoadingInfoTemporalSlice>;
490

491
    fn next(&mut self) -> Option<Self::Item> {
25✔
492
        let t1 = self.time_step_iter.next()?;
25✔
493

494
        // snap t1 relative to reference time
495
        let t1 = self.step.snap_relative(self.dataset_time_start, t1).ok()?;
21✔
496

497
        let t2 = t1 + self.step;
21✔
498
        let t2 = t2.unwrap_or(self.max_t2);
21✔
499

21✔
500
        let time_interval = TimeInterval::new_unchecked(t1, t2);
21✔
501

21✔
502
        // TODO: how to prevent generating loads of empty time intervals for a very small t1?
21✔
503
        if t1 < self.dataset_time_start {
21✔
504
            return Some(Ok(GdalLoadingInfoTemporalSlice {
2✔
505
                time: time_interval,
2✔
506
                params: None,
2✔
507
                cache_ttl: self.cache_ttl,
2✔
508
            }));
2✔
509
        }
19✔
510

19✔
511
        if t1 >= self.max_t2 {
19✔
512
            return None;
2✔
513
        }
17✔
514

515
        let steps_between = match self
17✔
516
            .step
17✔
517
            .num_steps_in_interval(TimeInterval::new_unchecked(self.dataset_time_start, t1))
17✔
518
        {
519
            Ok(num_steps) => num_steps,
17✔
520
            // should only happen when time intervals are faulty
521
            Err(error) => return Some(Err(error.into())),
×
522
        };
523

524
        // our first band is the reference time
525
        let mut params = self.params.clone();
17✔
526
        params.rasterband_channel =
17✔
527
            1 /* GDAL starts at 1 */ + self.band_offset + steps_between as usize;
17✔
528

17✔
529
        Some(Ok(GdalLoadingInfoTemporalSlice {
17✔
530
            time: time_interval,
17✔
531
            params: Some(params),
17✔
532
            cache_ttl: self.cache_ttl,
17✔
533
        }))
17✔
534
    }
25✔
535
}
536

537
#[derive(Debug, Clone)]
538
pub struct GdalLoadingInfo {
539
    /// partitions of dataset sorted by time
540
    pub info: GdalLoadingInfoTemporalSliceIterator,
541
    /// To answer the query time, the `TimeInterval` of the first issued part must contain the query start and the last issued part must contain the query end.
542
    /// This is the start of the first part produced to answer the query. If there is no data part that intersects the query start, this is the last time known before the query start.
543
    /// IF there is a part starting exactly at the query time start. It is the start time of that part (and the query).
544
    /// ELSE IF there is a known element with an end before the query time it is the end of that `TimeInterval`.
545
    /// ELSE IF there is no data at all (and never will be) it might be `TimeInstance::MIN`.
546
    /// ELSE it should be None and the `GdalSource` will replace it with the start of the query AND issue a warning about missing information.
547
    pub start_time_of_output_stream: Option<TimeInstance>,
548
    /// This is the end of the last part produced to answer the query.
549
    /// IF there is no data part that intersects the query end, this is the first time known after the query end.
550
    /// ELSE IF there is a known element with an end after the query time it is the start of that `TimeInterval`.
551
    /// ELSE IF there is no data at all (and never will be) it might be `TimeInstance::MAX`.
552
    /// ELSE it should be None and the `GdalSource` will replace it with the start of the query AND issue a warning about missing information.
553
    pub end_time_of_output_stream: Option<TimeInstance>,
554
}
555

556
impl GdalLoadingInfo {
557
    /// This method generates a new `GdalLoadingInfo`.
558
    /// The temporal slice iterator may not start/end covering the temporal part of the query. In this case the first temporal slice starts after the query start and there is a gap that has to be filled with a nodata tile.
559
    /// This nodata tile needs a temporal validity, thus we need to know how long before the start of the first loading info there is no data.
560
    /// This information is passed as `start/end_time_of_output_stream`.
561
    pub fn new(
134✔
562
        info: GdalLoadingInfoTemporalSliceIterator,
134✔
563
        start_time_of_output_stream: TimeInstance,
134✔
564
        end_time_of_output_stream: TimeInstance,
134✔
565
    ) -> Self {
134✔
566
        Self {
134✔
567
            info,
134✔
568
            start_time_of_output_stream: Some(start_time_of_output_stream),
134✔
569
            end_time_of_output_stream: Some(end_time_of_output_stream),
134✔
570
        }
134✔
571
    }
134✔
572

573
    /// NOTE! This method creates a new `GdalLoadingInfo` without information of the elements containing the query start/end `TimeInstance`.
574
    /// IF a provider can't determine where the prev. and/or following elements are placed in time use this method.
575
    pub fn new_no_known_time_bounds(info: GdalLoadingInfoTemporalSliceIterator) -> Self {
4✔
576
        Self {
4✔
577
            info,
4✔
578
            end_time_of_output_stream: None,
4✔
579
            start_time_of_output_stream: None,
4✔
580
        }
4✔
581
    }
4✔
582
}
583

584
#[allow(clippy::large_enum_variant)]
585
#[derive(Debug, Clone)]
586
pub enum GdalLoadingInfoTemporalSliceIterator {
587
    Static {
588
        parts: std::vec::IntoIter<GdalLoadingInfoTemporalSlice>,
589
    },
590
    Dynamic(DynamicGdalLoadingInfoPartIterator),
591
    NetCdfCf(NetCdfCfGdalLoadingInfoPartIterator),
592
}
593

594
impl Iterator for GdalLoadingInfoTemporalSliceIterator {
595
    type Item = Result<GdalLoadingInfoTemporalSlice>;
596

597
    fn next(&mut self) -> Option<Self::Item> {
290✔
598
        match self {
290✔
599
            GdalLoadingInfoTemporalSliceIterator::Static { parts } => parts.next().map(Result::Ok),
64✔
600
            GdalLoadingInfoTemporalSliceIterator::Dynamic(iter) => iter.next(),
222✔
601
            GdalLoadingInfoTemporalSliceIterator::NetCdfCf(iter) => iter.next(),
4✔
602
        }
603
    }
290✔
604
}
605

606
/// one temporal slice of the dataset that requires reading from exactly one Gdal dataset
607
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, FromSql, ToSql)]
1,431✔
608
#[serde(rename_all = "camelCase")]
609
pub struct GdalLoadingInfoTemporalSlice {
610
    pub time: TimeInterval,
611
    pub params: Option<GdalDatasetParameters>,
612
    #[serde(default)]
613
    pub cache_ttl: CacheTtlSeconds,
614
}
615

616
#[cfg(test)]
617
mod tests {
618
    use geoengine_datatypes::{
619
        hashmap,
620
        primitives::{
621
            BandSelection, DateTime, DateTimeParseFormat, SpatialPartition2D, SpatialResolution,
622
            TimeGranularity,
623
        },
624
        raster::RasterDataType,
625
        spatial_reference::SpatialReference,
626
        util::test::TestDefault,
627
    };
628

629
    use crate::{
630
        engine::RasterBandDescriptors,
631
        source::{FileNotFoundHandling, GdalDatasetGeoTransform, TimeReference},
632
    };
633

634
    use super::*;
635

636
    fn create_regular_metadata() -> GdalMetaDataRegular {
7✔
637
        let no_data_value = Some(0.);
7✔
638

7✔
639
        GdalMetaDataRegular {
7✔
640
            result_descriptor: RasterResultDescriptor {
7✔
641
                data_type: RasterDataType::U8,
7✔
642
                spatial_reference: SpatialReference::epsg_4326().into(),
7✔
643
                time: None,
7✔
644
                bbox: None,
7✔
645
                resolution: None,
7✔
646
                bands: RasterBandDescriptors::new_single_band(),
7✔
647
            },
7✔
648
            params: GdalDatasetParameters {
7✔
649
                file_path: "/foo/bar_%TIME%.tiff".into(),
7✔
650
                rasterband_channel: 0,
7✔
651
                geo_transform: TestDefault::test_default(),
7✔
652
                width: 360,
7✔
653
                height: 180,
7✔
654
                file_not_found_handling: FileNotFoundHandling::NoData,
7✔
655
                no_data_value,
7✔
656
                properties_mapping: None,
7✔
657
                gdal_open_options: None,
7✔
658
                gdal_config_options: None,
7✔
659
                allow_alphaband_as_mask: true,
7✔
660
                retry: None,
7✔
661
            },
7✔
662
            time_placeholders: hashmap! {
7✔
663
                "%TIME%".to_string() => GdalSourceTimePlaceholder {
7✔
664
                    format: DateTimeParseFormat::custom("%f".to_string()),
7✔
665
                    reference: TimeReference::Start,
7✔
666
                },
7✔
667
            },
7✔
668
            data_time: TimeInterval::new_unchecked(
7✔
669
                TimeInstance::from_millis_unchecked(0),
7✔
670
                TimeInstance::from_millis_unchecked(33),
7✔
671
            ),
7✔
672
            step: TimeStep {
7✔
673
                granularity: TimeGranularity::Millis,
7✔
674
                step: 11,
7✔
675
            },
7✔
676
            cache_ttl: CacheTtlSeconds::default(),
7✔
677
        }
7✔
678
    }
7✔
679

680
    #[tokio::test]
681
    async fn test_regular_meta_data_result_descriptor() {
1✔
682
        let meta_data = create_regular_metadata();
1✔
683

1✔
684
        assert_eq!(
1✔
685
            meta_data.result_descriptor().await.unwrap(),
1✔
686
            RasterResultDescriptor {
1✔
687
                data_type: RasterDataType::U8,
1✔
688
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
689
                time: None,
1✔
690
                bbox: None,
1✔
691
                resolution: None,
1✔
692
                bands: RasterBandDescriptors::new_single_band()
1✔
693
            }
1✔
694
        );
1✔
695
    }
1✔
696

697
    #[tokio::test]
698
    async fn test_regular_meta_data_0_30() {
1✔
699
        let meta_data = create_regular_metadata();
1✔
700

1✔
701
        assert_eq!(
1✔
702
            meta_data
1✔
703
                .loading_info(RasterQueryRectangle {
1✔
704
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
705
                        (0., 1.).into(),
1✔
706
                        (1., 0.).into()
1✔
707
                    ),
1✔
708
                    time_interval: TimeInterval::new_unchecked(0, 30),
1✔
709
                    spatial_resolution: SpatialResolution::one(),
1✔
710
                    attributes: BandSelection::first()
1✔
711
                })
1✔
712
                .await
1✔
713
                .unwrap()
1✔
714
                .info
1✔
715
                .map(|p| {
3✔
716
                    let p = p.unwrap();
3✔
717
                    (
3✔
718
                        p.time,
3✔
719
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
3✔
720
                    )
3✔
721
                })
3✔
722
                .collect::<Vec<_>>(),
1✔
723
            &[
1✔
724
                (
1✔
725
                    TimeInterval::new_unchecked(0, 11),
1✔
726
                    Some("/foo/bar_000000000.tiff".to_owned())
1✔
727
                ),
1✔
728
                (
1✔
729
                    TimeInterval::new_unchecked(11, 22),
1✔
730
                    Some("/foo/bar_011000000.tiff".to_owned())
1✔
731
                ),
1✔
732
                (
1✔
733
                    TimeInterval::new_unchecked(22, 33),
1✔
734
                    Some("/foo/bar_022000000.tiff".to_owned())
1✔
735
                ),
1✔
736
            ]
1✔
737
        );
1✔
738
    }
1✔
739

740
    #[tokio::test]
741
    async fn test_regular_meta_data_default_time() {
1✔
742
        let meta_data = create_regular_metadata();
1✔
743

1✔
744
        assert_eq!(
1✔
745
            meta_data
1✔
746
                .loading_info(RasterQueryRectangle {
1✔
747
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
748
                        (0., 1.).into(),
1✔
749
                        (1., 0.).into()
1✔
750
                    ),
1✔
751
                    time_interval: TimeInterval::default(),
1✔
752
                    spatial_resolution: SpatialResolution::one(),
1✔
753
                    attributes: BandSelection::first()
1✔
754
                })
1✔
755
                .await
1✔
756
                .unwrap()
1✔
757
                .info
1✔
758
                .map(|p| {
5✔
759
                    let p = p.unwrap();
5✔
760
                    (
5✔
761
                        p.time,
5✔
762
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
5✔
763
                    )
5✔
764
                })
5✔
765
                .collect::<Vec<_>>(),
1✔
766
            &[
1✔
767
                (TimeInterval::new_unchecked(TimeInstance::MIN, 0), None),
1✔
768
                (
1✔
769
                    TimeInterval::new_unchecked(0, 11),
1✔
770
                    Some("/foo/bar_000000000.tiff".to_owned())
1✔
771
                ),
1✔
772
                (
1✔
773
                    TimeInterval::new_unchecked(11, 22),
1✔
774
                    Some("/foo/bar_011000000.tiff".to_owned())
1✔
775
                ),
1✔
776
                (
1✔
777
                    TimeInterval::new_unchecked(22, 33),
1✔
778
                    Some("/foo/bar_022000000.tiff".to_owned())
1✔
779
                ),
1✔
780
                (TimeInterval::new_unchecked(33, TimeInstance::MAX), None)
1✔
781
            ]
1✔
782
        );
1✔
783
    }
1✔
784

785
    #[tokio::test]
786
    async fn test_regular_meta_data_before_data() {
1✔
787
        let meta_data = create_regular_metadata();
1✔
788

1✔
789
        assert_eq!(
1✔
790
            meta_data
1✔
791
                .loading_info(RasterQueryRectangle {
1✔
792
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
793
                        (0., 1.).into(),
1✔
794
                        (1., 0.).into()
1✔
795
                    ),
1✔
796
                    time_interval: TimeInterval::new_unchecked(-10, -5),
1✔
797
                    spatial_resolution: SpatialResolution::one(),
1✔
798
                    attributes: BandSelection::first()
1✔
799
                })
1✔
800
                .await
1✔
801
                .unwrap()
1✔
802
                .info
1✔
803
                .map(|p| {
1✔
804
                    let p = p.unwrap();
1✔
805
                    (
1✔
806
                        p.time,
1✔
807
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
1✔
808
                    )
1✔
809
                })
1✔
810
                .collect::<Vec<_>>(),
1✔
811
            &[(TimeInterval::new_unchecked(TimeInstance::MIN, 0), None),]
1✔
812
        );
1✔
813
    }
1✔
814

815
    #[tokio::test]
816
    async fn test_regular_meta_data_after_data() {
1✔
817
        let meta_data = create_regular_metadata();
1✔
818

1✔
819
        assert_eq!(
1✔
820
            meta_data
1✔
821
                .loading_info(RasterQueryRectangle {
1✔
822
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
823
                        (0., 1.).into(),
1✔
824
                        (1., 0.).into()
1✔
825
                    ),
1✔
826
                    time_interval: TimeInterval::new_unchecked(50, 55),
1✔
827
                    spatial_resolution: SpatialResolution::one(),
1✔
828
                    attributes: BandSelection::first()
1✔
829
                })
1✔
830
                .await
1✔
831
                .unwrap()
1✔
832
                .info
1✔
833
                .map(|p| {
1✔
834
                    let p = p.unwrap();
1✔
835
                    (
1✔
836
                        p.time,
1✔
837
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
1✔
838
                    )
1✔
839
                })
1✔
840
                .collect::<Vec<_>>(),
1✔
841
            &[(TimeInterval::new_unchecked(33, TimeInstance::MAX), None)]
1✔
842
        );
1✔
843
    }
1✔
844

845
    #[tokio::test]
846
    async fn test_regular_meta_data_0_22() {
1✔
847
        let meta_data = create_regular_metadata();
1✔
848

1✔
849
        assert_eq!(
1✔
850
            meta_data
1✔
851
                .loading_info(RasterQueryRectangle {
1✔
852
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
853
                        (0., 1.).into(),
1✔
854
                        (1., 0.).into()
1✔
855
                    ),
1✔
856
                    time_interval: TimeInterval::new_unchecked(0, 22),
1✔
857
                    spatial_resolution: SpatialResolution::one(),
1✔
858
                    attributes: BandSelection::first()
1✔
859
                })
1✔
860
                .await
1✔
861
                .unwrap()
1✔
862
                .info
1✔
863
                .map(|p| {
2✔
864
                    let p = p.unwrap();
2✔
865
                    (
2✔
866
                        p.time,
2✔
867
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
2✔
868
                    )
2✔
869
                })
2✔
870
                .collect::<Vec<_>>(),
1✔
871
            &[
1✔
872
                (
1✔
873
                    TimeInterval::new_unchecked(0, 11),
1✔
874
                    Some("/foo/bar_000000000.tiff".to_owned())
1✔
875
                ),
1✔
876
                (
1✔
877
                    TimeInterval::new_unchecked(11, 22),
1✔
878
                    Some("/foo/bar_011000000.tiff".to_owned())
1✔
879
                ),
1✔
880
            ]
1✔
881
        );
1✔
882
    }
1✔
883

884
    #[tokio::test]
885
    async fn test_regular_meta_data_0_20() {
1✔
886
        let meta_data = create_regular_metadata();
1✔
887

1✔
888
        assert_eq!(
1✔
889
            meta_data
1✔
890
                .loading_info(RasterQueryRectangle {
1✔
891
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
892
                        (0., 1.).into(),
1✔
893
                        (1., 0.).into()
1✔
894
                    ),
1✔
895
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
896
                    spatial_resolution: SpatialResolution::one(),
1✔
897
                    attributes: BandSelection::first()
1✔
898
                })
1✔
899
                .await
1✔
900
                .unwrap()
1✔
901
                .info
1✔
902
                .map(|p| {
2✔
903
                    let p = p.unwrap();
2✔
904
                    (
2✔
905
                        p.time,
2✔
906
                        p.params.map(|p| p.file_path.to_str().unwrap().to_owned()),
2✔
907
                    )
2✔
908
                })
2✔
909
                .collect::<Vec<_>>(),
1✔
910
            &[
1✔
911
                (
1✔
912
                    TimeInterval::new_unchecked(0, 11),
1✔
913
                    Some("/foo/bar_000000000.tiff".to_owned())
1✔
914
                ),
1✔
915
                (
1✔
916
                    TimeInterval::new_unchecked(11, 22),
1✔
917
                    Some("/foo/bar_011000000.tiff".to_owned())
1✔
918
                ),
1✔
919
            ]
1✔
920
        );
1✔
921
    }
1✔
922

923
    #[allow(clippy::too_many_lines)]
924
    #[tokio::test]
925
    async fn test_meta_data_list() {
1✔
926
        let no_data_value = Some(0.);
1✔
927

1✔
928
        let meta_data = GdalMetaDataList {
1✔
929
            result_descriptor: RasterResultDescriptor {
1✔
930
                data_type: RasterDataType::U8,
1✔
931
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
932
                time: None,
1✔
933
                bbox: None,
1✔
934
                resolution: None,
1✔
935
                bands: RasterBandDescriptors::new_single_band(),
1✔
936
            },
1✔
937
            params: vec![
1✔
938
                GdalLoadingInfoTemporalSlice {
1✔
939
                    time: TimeInterval::new_unchecked(0, 1),
1✔
940
                    params: Some(GdalDatasetParameters {
1✔
941
                        file_path: "/foo/bar_0.tiff".into(),
1✔
942
                        rasterband_channel: 0,
1✔
943
                        geo_transform: TestDefault::test_default(),
1✔
944
                        width: 360,
1✔
945
                        height: 180,
1✔
946
                        file_not_found_handling: FileNotFoundHandling::NoData,
1✔
947
                        no_data_value,
1✔
948
                        properties_mapping: None,
1✔
949
                        gdal_open_options: None,
1✔
950
                        gdal_config_options: None,
1✔
951
                        allow_alphaband_as_mask: true,
1✔
952
                        retry: None,
1✔
953
                    }),
1✔
954
                    cache_ttl: CacheTtlSeconds::default(),
1✔
955
                },
1✔
956
                GdalLoadingInfoTemporalSlice {
1✔
957
                    time: TimeInterval::new_unchecked(1, 5),
1✔
958
                    params: Some(GdalDatasetParameters {
1✔
959
                        file_path: "/foo/bar_1.tiff".into(),
1✔
960
                        rasterband_channel: 0,
1✔
961
                        geo_transform: TestDefault::test_default(),
1✔
962
                        width: 360,
1✔
963
                        height: 180,
1✔
964
                        file_not_found_handling: FileNotFoundHandling::NoData,
1✔
965
                        no_data_value,
1✔
966
                        properties_mapping: None,
1✔
967
                        gdal_open_options: None,
1✔
968
                        gdal_config_options: None,
1✔
969
                        allow_alphaband_as_mask: true,
1✔
970
                        retry: None,
1✔
971
                    }),
1✔
972
                    cache_ttl: CacheTtlSeconds::default(),
1✔
973
                },
1✔
974
                GdalLoadingInfoTemporalSlice {
1✔
975
                    time: TimeInterval::new_unchecked(5, 6),
1✔
976
                    params: Some(GdalDatasetParameters {
1✔
977
                        file_path: "/foo/bar_2.tiff".into(),
1✔
978
                        rasterband_channel: 0,
1✔
979
                        geo_transform: TestDefault::test_default(),
1✔
980
                        width: 360,
1✔
981
                        height: 180,
1✔
982
                        file_not_found_handling: FileNotFoundHandling::NoData,
1✔
983
                        no_data_value,
1✔
984
                        properties_mapping: None,
1✔
985
                        gdal_open_options: None,
1✔
986
                        gdal_config_options: None,
1✔
987
                        allow_alphaband_as_mask: true,
1✔
988
                        retry: None,
1✔
989
                    }),
1✔
990
                    cache_ttl: CacheTtlSeconds::default(),
1✔
991
                },
1✔
992
            ],
1✔
993
        };
1✔
994

1✔
995
        assert_eq!(
1✔
996
            meta_data.result_descriptor().await.unwrap(),
1✔
997
            RasterResultDescriptor {
1✔
998
                data_type: RasterDataType::U8,
1✔
999
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1000
                time: None,
1✔
1001
                bbox: None,
1✔
1002
                resolution: None,
1✔
1003
                bands: RasterBandDescriptors::new_single_band()
1✔
1004
            }
1✔
1005
        );
1✔
1006

1✔
1007
        assert_eq!(
1✔
1008
            meta_data
1✔
1009
                .loading_info(RasterQueryRectangle {
1✔
1010
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
1011
                        (0., 1.).into(),
1✔
1012
                        (1., 0.).into()
1✔
1013
                    ),
1✔
1014
                    time_interval: TimeInterval::new_unchecked(0, 3),
1✔
1015
                    spatial_resolution: SpatialResolution::one(),
1✔
1016
                    attributes: BandSelection::first()
1✔
1017
                })
1✔
1018
                .await
1✔
1019
                .unwrap()
1✔
1020
                .info
1✔
1021
                .map(|p| p
2✔
1022
                    .unwrap()
2✔
1023
                    .params
2✔
1024
                    .unwrap()
2✔
1025
                    .file_path
2✔
1026
                    .to_str()
2✔
1027
                    .unwrap()
2✔
1028
                    .to_owned())
2✔
1029
                .collect::<Vec<_>>(),
1✔
1030
            &["/foo/bar_0.tiff", "/foo/bar_1.tiff",]
1✔
1031
        );
1✔
1032
    }
1✔
1033

1034
    #[tokio::test]
1035
    async fn netcdf_cf_single_time_step() {
1✔
1036
        let time_start = TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0));
1✔
1037
        let time_end = TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0));
1✔
1038
        let time_step = TimeStep {
1✔
1039
            step: 0,
1✔
1040
            granularity: TimeGranularity::Years,
1✔
1041
        };
1✔
1042

1✔
1043
        let metadata = GdalMetadataNetCdfCf {
1✔
1044
            result_descriptor: RasterResultDescriptor {
1✔
1045
                data_type: RasterDataType::U8,
1✔
1046
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1047
                time: None,
1✔
1048
                bbox: None,
1✔
1049
                resolution: None,
1✔
1050
                bands: RasterBandDescriptors::new_single_band(),
1✔
1051
            },
1✔
1052
            params: GdalDatasetParameters {
1✔
1053
                file_path: "path/to/ds".into(),
1✔
1054
                rasterband_channel: 0,
1✔
1055
                geo_transform: GdalDatasetGeoTransform {
1✔
1056
                    origin_coordinate: (0., 0.).into(),
1✔
1057
                    x_pixel_size: 1.,
1✔
1058
                    y_pixel_size: 1.,
1✔
1059
                },
1✔
1060
                width: 128,
1✔
1061
                height: 128,
1✔
1062
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1063
                no_data_value: None,
1✔
1064
                properties_mapping: None,
1✔
1065
                gdal_open_options: None,
1✔
1066
                gdal_config_options: None,
1✔
1067
                allow_alphaband_as_mask: true,
1✔
1068
                retry: None,
1✔
1069
            },
1✔
1070
            start: time_start,
1✔
1071
            end: time_end,
1✔
1072
            step: time_step,
1✔
1073
            band_offset: 0,
1✔
1074
            cache_ttl: CacheTtlSeconds::default(),
1✔
1075
        };
1✔
1076

1✔
1077
        let query = RasterQueryRectangle {
1✔
1078
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 128.).into(), (128., 0.).into()),
1✔
1079
            time_interval: TimeInterval::new(time_start, time_end).unwrap(),
1✔
1080
            spatial_resolution: SpatialResolution::one(),
1✔
1081
            attributes: BandSelection::first(),
1✔
1082
        };
1✔
1083

1✔
1084
        let loading_info = metadata.loading_info(query).await.unwrap();
1✔
1085
        let mut iter = loading_info.info;
1✔
1086

1✔
1087
        let step_1 = iter.next().unwrap().unwrap();
1✔
1088

1✔
1089
        assert_eq!(
1✔
1090
            step_1.time,
1✔
1091
            TimeInterval::new(
1✔
1092
                TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0)),
1✔
1093
                TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0))
1✔
1094
            )
1✔
1095
            .unwrap()
1✔
1096
        );
1✔
1097
        assert_eq!(step_1.params.unwrap().rasterband_channel, 1);
1✔
1098

1✔
1099
        assert!(iter.next().is_none());
1✔
1100
    }
1✔
1101

1102
    #[tokio::test]
1103
    async fn netcdf_cf_single_time_step_with_offset() {
1✔
1104
        let time_start = TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0));
1✔
1105
        let time_end = TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0));
1✔
1106
        let time_step = TimeStep {
1✔
1107
            step: 0,
1✔
1108
            granularity: TimeGranularity::Years,
1✔
1109
        };
1✔
1110

1✔
1111
        let metadata = GdalMetadataNetCdfCf {
1✔
1112
            result_descriptor: RasterResultDescriptor {
1✔
1113
                data_type: RasterDataType::U8,
1✔
1114
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1115
                time: None,
1✔
1116
                bbox: None,
1✔
1117
                resolution: None,
1✔
1118
                bands: RasterBandDescriptors::new_single_band(),
1✔
1119
            },
1✔
1120
            params: GdalDatasetParameters {
1✔
1121
                file_path: "path/to/ds".into(),
1✔
1122
                rasterband_channel: 0,
1✔
1123
                geo_transform: GdalDatasetGeoTransform {
1✔
1124
                    origin_coordinate: (0., 0.).into(),
1✔
1125
                    x_pixel_size: 1.,
1✔
1126
                    y_pixel_size: 1.,
1✔
1127
                },
1✔
1128
                width: 128,
1✔
1129
                height: 128,
1✔
1130
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1131
                no_data_value: None,
1✔
1132
                properties_mapping: None,
1✔
1133
                gdal_open_options: None,
1✔
1134
                gdal_config_options: None,
1✔
1135
                allow_alphaband_as_mask: true,
1✔
1136
                retry: None,
1✔
1137
            },
1✔
1138
            start: time_start,
1✔
1139
            end: time_end,
1✔
1140
            step: time_step,
1✔
1141
            band_offset: 1,
1✔
1142
            cache_ttl: CacheTtlSeconds::default(),
1✔
1143
        };
1✔
1144

1✔
1145
        let query = RasterQueryRectangle {
1✔
1146
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 128.).into(), (128., 0.).into()),
1✔
1147
            time_interval: TimeInterval::new(time_start, time_end).unwrap(),
1✔
1148
            spatial_resolution: SpatialResolution::one(),
1✔
1149
            attributes: BandSelection::first(),
1✔
1150
        };
1✔
1151

1✔
1152
        let loading_info = metadata.loading_info(query).await.unwrap();
1✔
1153
        let mut iter = loading_info.info;
1✔
1154

1✔
1155
        let step_1 = iter.next().unwrap().unwrap();
1✔
1156

1✔
1157
        assert_eq!(
1✔
1158
            step_1.time,
1✔
1159
            TimeInterval::new(
1✔
1160
                TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0)),
1✔
1161
                TimeInstance::from(DateTime::new_utc(2000, 1, 1, 0, 0, 0))
1✔
1162
            )
1✔
1163
            .unwrap()
1✔
1164
        );
1✔
1165
        assert_eq!(step_1.params.unwrap().rasterband_channel, 2);
1✔
1166

1✔
1167
        assert!(iter.next().is_none());
1✔
1168
    }
1✔
1169

1170
    #[tokio::test]
1171
    async fn netcdf_cf_time_steps_before_after() {
1✔
1172
        let time_start = TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0));
1✔
1173
        let time_end = TimeInstance::from(DateTime::new_utc(2012, 1, 1, 0, 0, 0));
1✔
1174
        let time_step = TimeStep {
1✔
1175
            step: 1,
1✔
1176
            granularity: TimeGranularity::Years,
1✔
1177
        };
1✔
1178

1✔
1179
        let metadata = GdalMetadataNetCdfCf {
1✔
1180
            result_descriptor: RasterResultDescriptor {
1✔
1181
                data_type: RasterDataType::U8,
1✔
1182
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1183
                time: None,
1✔
1184
                bbox: None,
1✔
1185
                resolution: None,
1✔
1186
                bands: RasterBandDescriptors::new_single_band(),
1✔
1187
            },
1✔
1188
            params: GdalDatasetParameters {
1✔
1189
                file_path: "path/to/ds".into(),
1✔
1190
                rasterband_channel: 0,
1✔
1191
                geo_transform: GdalDatasetGeoTransform {
1✔
1192
                    origin_coordinate: (0., 0.).into(),
1✔
1193
                    x_pixel_size: 1.,
1✔
1194
                    y_pixel_size: 1.,
1✔
1195
                },
1✔
1196
                width: 128,
1✔
1197
                height: 128,
1✔
1198
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1199
                no_data_value: None,
1✔
1200
                properties_mapping: None,
1✔
1201
                gdal_open_options: None,
1✔
1202
                gdal_config_options: None,
1✔
1203
                allow_alphaband_as_mask: true,
1✔
1204
                retry: None,
1✔
1205
            },
1✔
1206
            start: time_start,
1✔
1207
            end: time_end,
1✔
1208
            step: time_step,
1✔
1209
            band_offset: 0,
1✔
1210
            cache_ttl: CacheTtlSeconds::default(),
1✔
1211
        };
1✔
1212

1✔
1213
        let query = RasterQueryRectangle {
1✔
1214
            spatial_bounds: SpatialPartition2D::new_unchecked((0., 128.).into(), (128., 0.).into()),
1✔
1215
            time_interval: TimeInterval::new_unchecked(
1✔
1216
                TimeInstance::from(DateTime::new_utc(2009, 7, 1, 0, 0, 0)),
1✔
1217
                TimeInstance::from(DateTime::new_utc(2013, 3, 1, 0, 0, 0)),
1✔
1218
            ),
1✔
1219
            spatial_resolution: SpatialResolution::one(),
1✔
1220
            attributes: BandSelection::first(),
1✔
1221
        };
1✔
1222

1✔
1223
        let loading_info = metadata.loading_info(query).await.unwrap();
1✔
1224
        let mut iter = loading_info.info;
1✔
1225

1✔
1226
        let step_1 = iter.next().unwrap().unwrap();
1✔
1227

1✔
1228
        assert_eq!(
1✔
1229
            step_1.time,
1✔
1230
            TimeInterval::new(
1✔
1231
                TimeInstance::from(DateTime::new_utc(2009, 1, 1, 0, 0, 0)),
1✔
1232
                TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0))
1✔
1233
            )
1✔
1234
            .unwrap()
1✔
1235
        );
1✔
1236
        assert!(step_1.params.is_none());
1✔
1237

1✔
1238
        let step_2 = iter.next().unwrap().unwrap();
1✔
1239

1✔
1240
        assert_eq!(
1✔
1241
            step_2.time,
1✔
1242
            TimeInterval::new(
1✔
1243
                TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0)),
1✔
1244
                TimeInstance::from(DateTime::new_utc(2011, 1, 1, 0, 0, 0))
1✔
1245
            )
1✔
1246
            .unwrap()
1✔
1247
        );
1✔
1248
        assert_eq!(step_2.params.unwrap().rasterband_channel, 1);
1✔
1249

1✔
1250
        let step_3 = iter.next().unwrap().unwrap();
1✔
1251

1✔
1252
        assert_eq!(
1✔
1253
            step_3.time,
1✔
1254
            TimeInterval::new(
1✔
1255
                TimeInstance::from(DateTime::new_utc(2011, 1, 1, 0, 0, 0)),
1✔
1256
                TimeInstance::from(DateTime::new_utc(2012, 1, 1, 0, 0, 0))
1✔
1257
            )
1✔
1258
            .unwrap()
1✔
1259
        );
1✔
1260
        assert_eq!(step_3.params.unwrap().rasterband_channel, 2);
1✔
1261

1✔
1262
        assert!(iter.next().is_none());
1✔
1263
    }
1✔
1264

1265
    #[test]
1266
    fn netcdf_cf_time_steps() {
1✔
1267
        let time_start = TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0));
1✔
1268
        let time_end = TimeInstance::from(DateTime::new_utc(2022, 1, 1, 0, 0, 0));
1✔
1269
        let time_step = TimeStep {
1✔
1270
            step: 1,
1✔
1271
            granularity: TimeGranularity::Years,
1✔
1272
        };
1✔
1273
        let mut iter = NetCdfCfGdalLoadingInfoPartIterator {
1✔
1274
            time_step_iter: TimeStepIter::new_with_interval(
1✔
1275
                TimeInterval::new(time_start, time_end).unwrap(),
1✔
1276
                time_step,
1✔
1277
            )
1✔
1278
            .unwrap(),
1✔
1279
            params: GdalDatasetParameters {
1✔
1280
                file_path: "path/to/ds".into(),
1✔
1281
                rasterband_channel: 0,
1✔
1282
                geo_transform: GdalDatasetGeoTransform {
1✔
1283
                    origin_coordinate: (0., 0.).into(),
1✔
1284
                    x_pixel_size: 1.,
1✔
1285
                    y_pixel_size: 1.,
1✔
1286
                },
1✔
1287
                width: 128,
1✔
1288
                height: 128,
1✔
1289
                file_not_found_handling: FileNotFoundHandling::Error,
1✔
1290
                no_data_value: None,
1✔
1291
                properties_mapping: None,
1✔
1292
                gdal_open_options: None,
1✔
1293
                gdal_config_options: None,
1✔
1294
                allow_alphaband_as_mask: true,
1✔
1295
                retry: None,
1✔
1296
            },
1✔
1297
            step: time_step,
1✔
1298
            dataset_time_start: time_start,
1✔
1299
            max_t2: time_end,
1✔
1300
            band_offset: 0,
1✔
1301
            cache_ttl: CacheTtlSeconds::default(),
1✔
1302
        };
1✔
1303

1✔
1304
        let step_1 = iter.next().unwrap().unwrap();
1✔
1305

1✔
1306
        assert_eq!(
1✔
1307
            step_1.time,
1✔
1308
            TimeInterval::new(
1✔
1309
                TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0)),
1✔
1310
                TimeInstance::from(DateTime::new_utc(2011, 1, 1, 0, 0, 0))
1✔
1311
            )
1✔
1312
            .unwrap()
1✔
1313
        );
1✔
1314
        assert_eq!(step_1.params.unwrap().rasterband_channel, 1);
1✔
1315

1316
        let step_2 = iter.next().unwrap().unwrap();
1✔
1317

1✔
1318
        assert_eq!(
1✔
1319
            step_2.time,
1✔
1320
            TimeInterval::new(
1✔
1321
                TimeInstance::from(DateTime::new_utc(2011, 1, 1, 0, 0, 0)),
1✔
1322
                TimeInstance::from(DateTime::new_utc(2012, 1, 1, 0, 0, 0))
1✔
1323
            )
1✔
1324
            .unwrap()
1✔
1325
        );
1✔
1326
        assert_eq!(step_2.params.unwrap().rasterband_channel, 2);
1✔
1327

1328
        let step_3 = iter.next().unwrap().unwrap();
1✔
1329

1✔
1330
        assert_eq!(
1✔
1331
            step_3.time,
1✔
1332
            TimeInterval::new(
1✔
1333
                TimeInstance::from(DateTime::new_utc(2012, 1, 1, 0, 0, 0)),
1✔
1334
                TimeInstance::from(DateTime::new_utc(2013, 1, 1, 0, 0, 0))
1✔
1335
            )
1✔
1336
            .unwrap()
1✔
1337
        );
1✔
1338
        assert_eq!(step_3.params.unwrap().rasterband_channel, 3);
1✔
1339

1340
        for i in 4..=12 {
10✔
1341
            let step = iter.next().unwrap().unwrap();
9✔
1342

9✔
1343
            assert_eq!(step.params.unwrap().rasterband_channel, i);
9✔
1344
        }
1345

1346
        assert!(iter.next().is_none());
1✔
1347
    }
1✔
1348

1349
    #[test]
1350
    fn netcdf_cf_time_step_instant() {
1✔
1351
        fn iter_for_instance(instance: TimeInstance) -> NetCdfCfGdalLoadingInfoPartIterator {
5✔
1352
            let time_step = TimeStep {
5✔
1353
                step: 1,
5✔
1354
                granularity: TimeGranularity::Years,
5✔
1355
            };
5✔
1356
            NetCdfCfGdalLoadingInfoPartIterator {
5✔
1357
                time_step_iter: TimeStepIter::new_with_interval(
5✔
1358
                    TimeInterval::new_instant(instance).unwrap(),
5✔
1359
                    time_step,
5✔
1360
                )
5✔
1361
                .unwrap(),
5✔
1362
                params: GdalDatasetParameters {
5✔
1363
                    file_path: "path/to/ds".into(),
5✔
1364
                    rasterband_channel: 0,
5✔
1365
                    geo_transform: GdalDatasetGeoTransform {
5✔
1366
                        origin_coordinate: (0., 0.).into(),
5✔
1367
                        x_pixel_size: 1.,
5✔
1368
                        y_pixel_size: 1.,
5✔
1369
                    },
5✔
1370
                    width: 128,
5✔
1371
                    height: 128,
5✔
1372
                    file_not_found_handling: FileNotFoundHandling::Error,
5✔
1373
                    no_data_value: None,
5✔
1374
                    properties_mapping: None,
5✔
1375
                    gdal_open_options: None,
5✔
1376
                    gdal_config_options: None,
5✔
1377
                    allow_alphaband_as_mask: true,
5✔
1378
                    retry: None,
5✔
1379
                },
5✔
1380
                step: time_step,
5✔
1381
                dataset_time_start: TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0)),
5✔
1382
                max_t2: TimeInstance::from(DateTime::new_utc(2022, 1, 1, 0, 0, 0)),
5✔
1383
                band_offset: 0,
5✔
1384
                cache_ttl: CacheTtlSeconds::default(),
5✔
1385
            }
5✔
1386
        }
5✔
1387

1388
        let mut iter =
1✔
1389
            iter_for_instance(TimeInstance::from(DateTime::new_utc(2014, 1, 1, 0, 0, 0)));
1✔
1390

1✔
1391
        let step = iter.next().unwrap().unwrap();
1✔
1392

1✔
1393
        assert_eq!(
1✔
1394
            step.time,
1✔
1395
            TimeInterval::new(
1✔
1396
                TimeInstance::from(DateTime::new_utc(2014, 1, 1, 0, 0, 0)),
1✔
1397
                TimeInstance::from(DateTime::new_utc(2015, 1, 1, 0, 0, 0))
1✔
1398
            )
1✔
1399
            .unwrap()
1✔
1400
        );
1✔
1401
        assert_eq!(step.params.unwrap().rasterband_channel, 5);
1✔
1402

1403
        assert!(iter.next().is_none());
1✔
1404

1405
        let mut iter =
1✔
1406
            iter_for_instance(TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0)));
1✔
1407

1✔
1408
        let step = iter.next().unwrap().unwrap();
1✔
1409

1✔
1410
        assert_eq!(
1✔
1411
            step.time,
1✔
1412
            TimeInterval::new(
1✔
1413
                TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0)),
1✔
1414
                TimeInstance::from(DateTime::new_utc(2011, 1, 1, 0, 0, 0))
1✔
1415
            )
1✔
1416
            .unwrap()
1✔
1417
        );
1✔
1418
        assert_eq!(step.params.unwrap().rasterband_channel, 1);
1✔
1419

1420
        assert!(iter.next().is_none());
1✔
1421

1422
        let mut iter =
1✔
1423
            iter_for_instance(TimeInstance::from(DateTime::new_utc(2021, 1, 1, 0, 0, 0)));
1✔
1424

1✔
1425
        let step = iter.next().unwrap().unwrap();
1✔
1426

1✔
1427
        assert_eq!(
1✔
1428
            step.time,
1✔
1429
            TimeInterval::new(
1✔
1430
                TimeInstance::from(DateTime::new_utc(2021, 1, 1, 0, 0, 0)),
1✔
1431
                TimeInstance::from(DateTime::new_utc(2022, 1, 1, 0, 0, 0))
1✔
1432
            )
1✔
1433
            .unwrap()
1✔
1434
        );
1✔
1435
        assert_eq!(step.params.unwrap().rasterband_channel, 12);
1✔
1436

1437
        assert!(iter.next().is_none());
1✔
1438

1439
        let iter = iter_for_instance(TimeInstance::from(DateTime::new_utc(2009, 1, 1, 0, 0, 0)))
1✔
1440
            .next()
1✔
1441
            .unwrap()
1✔
1442
            .unwrap();
1✔
1443
        assert_eq!(
1✔
1444
            iter.time,
1✔
1445
            TimeInterval::new(
1✔
1446
                TimeInstance::from(DateTime::new_utc(2009, 1, 1, 0, 0, 0)),
1✔
1447
                TimeInstance::from(DateTime::new_utc(2010, 1, 1, 0, 0, 0))
1✔
1448
            )
1✔
1449
            .unwrap(),
1✔
1450
        );
1✔
1451
        assert!(iter.params.is_none());
1✔
1452

1453
        assert!(
1✔
1454
            iter_for_instance(TimeInstance::from(DateTime::new_utc(2022, 1, 1, 0, 0, 0),))
1✔
1455
                .next()
1✔
1456
                .is_none()
1✔
1457
        );
1✔
1458
    }
1✔
1459
}
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