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

geo-engine / geoengine / 13809415963

12 Mar 2025 10:42AM UTC coverage: 90.026% (-0.05%) from 90.076%
13809415963

Pull #1013

github

web-flow
Merge b51e2554c into c96026921
Pull Request #1013: Update-utoipa

787 of 935 new or added lines in 41 files covered. (84.17%)

28 existing lines in 10 files now uncovered.

125995 of 139954 relevant lines covered (90.03%)

57510.86 hits per line

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

88.54
/services/src/datasets/external/sentinel_s2_l2a_cogs.rs
1
use crate::contexts::GeoEngineDb;
2
use crate::datasets::listing::ProvenanceOutput;
3
use crate::error::{self, Error, Result};
4
use crate::layers::external::{DataProvider, DataProviderDefinition};
5
use crate::layers::layer::{
6
    CollectionItem, Layer, LayerCollection, LayerCollectionListOptions, LayerListing,
7
    ProviderLayerCollectionId, ProviderLayerId,
8
};
9
use crate::layers::listing::{
10
    LayerCollectionId, LayerCollectionProvider, ProviderCapabilities, SearchCapabilities,
11
};
12
use crate::projects::{RasterSymbology, Symbology};
13
use crate::stac::{Feature as StacFeature, FeatureCollection as StacCollection, StacAsset};
14
use crate::util::operators::source_operator_from_dataset;
15
use crate::workflows::workflow::Workflow;
16
use async_trait::async_trait;
17
use geoengine_datatypes::dataset::{DataId, DataProviderId, LayerId, NamedData};
18
use geoengine_datatypes::operations::image::{RasterColorizer, RgbaColor};
19
use geoengine_datatypes::operations::reproject::{
20
    CoordinateProjection, CoordinateProjector, ReprojectClipped,
21
};
22
use geoengine_datatypes::primitives::CacheTtlSeconds;
23
use geoengine_datatypes::primitives::{
24
    AxisAlignedRectangle, BoundingBox2D, DateTime, Duration, RasterQueryRectangle,
25
    SpatialPartitioned, TimeInstance, TimeInterval, VectorQueryRectangle,
26
};
27
use geoengine_datatypes::raster::RasterDataType;
28
use geoengine_datatypes::spatial_reference::{SpatialReference, SpatialReferenceAuthority};
29
use geoengine_operators::engine::{
30
    MetaData, MetaDataProvider, OperatorName, RasterBandDescriptors, RasterOperator,
31
    RasterResultDescriptor, TypedOperator, VectorResultDescriptor,
32
};
33
use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo;
34
use geoengine_operators::source::{
35
    GdalDatasetGeoTransform, GdalDatasetParameters, GdalLoadingInfo, GdalLoadingInfoTemporalSlice,
36
    GdalLoadingInfoTemporalSliceIterator, GdalRetryOptions, GdalSource, GdalSourceParameters,
37
    OgrSourceDataset,
38
};
39
use geoengine_operators::util::retry::retry;
40
use log::debug;
41
use postgres_types::{FromSql, ToSql};
42
use reqwest::Client;
43
use serde::{Deserialize, Serialize};
44
use snafu::{ensure, ResultExt};
45
use std::collections::HashMap;
46
use std::convert::TryInto;
47
use std::fmt::Debug;
48
use std::path::PathBuf;
49

50
static STAC_RETRY_MAX_BACKOFF_MS: u64 = 60 * 60 * 1000;
51

52
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, FromSql, ToSql)]
×
53
#[serde(rename_all = "camelCase")]
54
pub struct SentinelS2L2ACogsProviderDefinition {
55
    pub name: String,
56
    pub id: DataProviderId,
57
    pub description: String,
58
    pub priority: Option<i16>,
59
    pub api_url: String,
60
    pub bands: Vec<StacBand>,
61
    pub zones: Vec<StacZone>,
62
    #[serde(default)]
63
    pub stac_api_retries: StacApiRetries,
64
    #[serde(default)]
65
    pub gdal_retries: GdalRetries,
66
    #[serde(default)]
67
    pub cache_ttl: CacheTtlSeconds,
68
    #[serde(default)]
69
    pub query_buffer: StacQueryBuffer,
70
}
71

72
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, FromSql, ToSql)]
×
73
#[serde(rename_all = "camelCase")]
74
/// A struct that represents buffers to apply to stac requests
75
pub struct StacQueryBuffer {
76
    pub start_seconds: i64,
77
    pub end_seconds: i64,
78
    // TODO: add also spatial buffers?
79
}
80

81
impl Default for StacQueryBuffer {
82
    fn default() -> Self {
1✔
83
        Self {
1✔
84
            start_seconds: 60,
1✔
85
            end_seconds: 60,
1✔
86
        }
1✔
87
    }
1✔
88
}
89

90
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
91
#[serde(rename_all = "camelCase")]
92
pub struct StacApiRetries {
93
    pub number_of_retries: usize,
94
    pub initial_delay_ms: u64,
95
    pub exponential_backoff_factor: f64,
96
}
97

98
impl Default for StacApiRetries {
99
    // TODO: find good defaults
100
    fn default() -> Self {
4✔
101
        Self {
4✔
102
            number_of_retries: 3,
4✔
103
            initial_delay_ms: 125,
4✔
104
            exponential_backoff_factor: 2.,
4✔
105
        }
4✔
106
    }
4✔
107
}
108

109
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
110
#[serde(rename_all = "camelCase")]
111
pub struct GdalRetries {
112
    /// retry at most `number_of_retries` times with exponential backoff
113
    pub number_of_retries: usize,
114
}
115

116
impl Default for GdalRetries {
117
    fn default() -> Self {
3✔
118
        Self {
3✔
119
            number_of_retries: 10,
3✔
120
        }
3✔
121
    }
3✔
122
}
123

124
#[async_trait]
125
impl<D: GeoEngineDb> DataProviderDefinition<D> for SentinelS2L2ACogsProviderDefinition {
126
    async fn initialize(self: Box<Self>, _db: D) -> crate::error::Result<Box<dyn DataProvider>> {
4✔
127
        Ok(Box::new(SentinelS2L2aCogsDataProvider::new(
4✔
128
            self.id,
4✔
129
            self.name,
4✔
130
            self.description,
4✔
131
            self.api_url,
4✔
132
            &self.bands,
4✔
133
            &self.zones,
4✔
134
            self.stac_api_retries,
4✔
135
            self.gdal_retries,
4✔
136
            self.cache_ttl,
4✔
137
            self.query_buffer,
4✔
138
        )))
4✔
139
    }
8✔
140

141
    fn type_name(&self) -> &'static str {
×
142
        "SentinelS2L2ACogs"
×
143
    }
×
144

145
    fn name(&self) -> String {
1✔
146
        self.name.clone()
1✔
147
    }
1✔
148

149
    fn id(&self) -> geoengine_datatypes::dataset::DataProviderId {
1✔
150
        self.id
1✔
151
    }
1✔
152

153
    fn priority(&self) -> i16 {
1✔
154
        self.priority.unwrap_or(0)
1✔
155
    }
1✔
156
}
157

158
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, FromSql, ToSql)]
×
159
#[serde(rename_all = "camelCase")]
160
pub struct StacBand {
161
    pub name: String,
162
    pub no_data_value: Option<f64>,
163
    pub data_type: RasterDataType,
164
}
165

166
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, FromSql, ToSql)]
×
167
pub struct StacZone {
168
    pub name: String,
169
    pub epsg: u32,
170
}
171

172
#[derive(Debug, Clone, PartialEq)]
173
pub struct SentinelDataset {
174
    band: StacBand,
175
    zone: StacZone,
176
    listing: Layer,
177
}
178

179
#[derive(Debug)]
180
pub struct SentinelS2L2aCogsDataProvider {
181
    id: DataProviderId,
182

183
    name: String,
184
    description: String,
185

186
    api_url: String,
187

188
    datasets: HashMap<LayerId, SentinelDataset>,
189

190
    stac_api_retries: StacApiRetries,
191
    gdal_retries: GdalRetries,
192

193
    cache_ttl: CacheTtlSeconds,
194

195
    query_buffer: StacQueryBuffer,
196
}
197

198
impl SentinelS2L2aCogsDataProvider {
199
    #[allow(clippy::too_many_arguments)]
200
    pub fn new(
4✔
201
        id: DataProviderId,
4✔
202
        name: String,
4✔
203
        description: String,
4✔
204
        api_url: String,
4✔
205
        bands: &[StacBand],
4✔
206
        zones: &[StacZone],
4✔
207
        stac_api_retries: StacApiRetries,
4✔
208
        gdal_retries: GdalRetries,
4✔
209
        cache_ttl: CacheTtlSeconds,
4✔
210
        query_buffer: StacQueryBuffer,
4✔
211
    ) -> Self {
4✔
212
        Self {
4✔
213
            id,
4✔
214
            name,
4✔
215
            description,
4✔
216
            api_url,
4✔
217
            datasets: Self::create_datasets(&id, bands, zones),
4✔
218
            stac_api_retries,
4✔
219
            gdal_retries,
4✔
220
            cache_ttl,
4✔
221
            query_buffer,
4✔
222
        }
4✔
223
    }
4✔
224

225
    fn create_datasets(
4✔
226
        id: &DataProviderId,
4✔
227
        bands: &[StacBand],
4✔
228
        zones: &[StacZone],
4✔
229
    ) -> HashMap<LayerId, SentinelDataset> {
4✔
230
        zones
4✔
231
            .iter()
4✔
232
            .flat_map(|zone| {
16✔
233
                bands.iter().map(move |band| {
91✔
234
                    let layer_id = LayerId(format!("{}:{}", zone.name, band.name));
91✔
235
                    let listing = Layer {
91✔
236
                        id: ProviderLayerId {
91✔
237
                            provider_id: *id,
91✔
238
                            layer_id: layer_id.clone(),
91✔
239
                        },
91✔
240
                        name: format!("Sentinel S2 L2A COGS {}:{}", zone.name, band.name),
91✔
241
                        description: String::new(),
91✔
242
                        workflow: Workflow {
91✔
243
                            operator: source_operator_from_dataset(
91✔
244
                                GdalSource::TYPE_NAME,
91✔
245
                                &NamedData {
91✔
246
                                    namespace: None,
91✔
247
                                    provider: Some(id.to_string()),
91✔
248
                                    name: layer_id.to_string(),
91✔
249
                                },
91✔
250
                            )
91✔
251
                            .expect("Gdal source is a valid operator."),
91✔
252
                        },
91✔
253
                        symbology: Some(Symbology::Raster(RasterSymbology {
91✔
254
                            opacity: 1.0,
91✔
255
                            raster_colorizer: RasterColorizer::SingleBand {
91✔
256
                                band: 0, band_colorizer:
91✔
257
                                geoengine_datatypes::operations::image::Colorizer::linear_gradient(
91✔
258
                                    vec![
91✔
259
                                        (0.0, RgbaColor::white())
91✔
260
                                            .try_into()
91✔
261
                                            .expect("valid breakpoint"),
91✔
262
                                        (10_000.0, RgbaColor::black())
91✔
263
                                            .try_into()
91✔
264
                                            .expect("valid breakpoint"),
91✔
265
                                    ],
91✔
266
                                    RgbaColor::transparent(),
91✔
267
                                    RgbaColor::white(),
91✔
268
                                    RgbaColor::black(),
91✔
269
                                )
91✔
270
                                .expect("valid colorizer"),
91✔
271
                        }})), // TODO: individual colorizer per band
91✔
272
                        properties: vec![],
91✔
273
                        metadata: HashMap::new(),
91✔
274
                    };
91✔
275

91✔
276
                    let dataset = SentinelDataset {
91✔
277
                        zone: zone.clone(),
91✔
278
                        band: band.clone(),
91✔
279
                        listing,
91✔
280
                    };
91✔
281

91✔
282
                    (layer_id, dataset)
91✔
283
                })
91✔
284
            })
16✔
285
            .collect()
4✔
286
    }
4✔
287
}
288

289
#[async_trait]
290
impl DataProvider for SentinelS2L2aCogsDataProvider {
291
    async fn provenance(&self, id: &DataId) -> Result<ProvenanceOutput> {
×
292
        Ok(ProvenanceOutput {
×
293
            data: id.clone(),
×
294
            provenance: None, // TODO
×
295
        })
×
296
    }
×
297
}
298

299
#[async_trait]
300
impl LayerCollectionProvider for SentinelS2L2aCogsDataProvider {
301
    fn capabilities(&self) -> ProviderCapabilities {
×
302
        ProviderCapabilities {
×
303
            listing: true,
×
304
            search: SearchCapabilities::none(),
×
305
        }
×
306
    }
×
307

308
    fn name(&self) -> &str {
×
309
        &self.name
×
310
    }
×
311

312
    fn description(&self) -> &str {
×
313
        &self.description
×
314
    }
×
315

316
    async fn load_layer_collection(
317
        &self,
318
        collection: &LayerCollectionId,
319
        options: LayerCollectionListOptions,
320
    ) -> Result<LayerCollection> {
×
321
        ensure!(
×
322
            *collection == self.get_root_layer_collection_id().await?,
×
323
            error::UnknownLayerCollectionId {
×
324
                id: collection.clone()
×
325
            }
×
326
        );
327

328
        let mut items = self
×
329
            .datasets
×
330
            .values()
×
331
            .map(|d| {
×
NEW
332
                Ok(CollectionItem::Layer(LayerListing { r#type: Default::default(),
×
333
                    id: d.listing.id.clone(),
×
334
                    name: d.listing.name.clone(),
×
335
                    description: d.listing.description.clone(),
×
336
                    properties: vec![],
×
337
                }))
×
338
            })
×
339
            .collect::<Result<Vec<CollectionItem>>>()?;
×
340
        items.sort_by_key(|e| e.name().to_string());
×
341

×
342
        let items = items
×
343
            .into_iter()
×
344
            .skip(options.offset as usize)
×
345
            .take(options.limit as usize)
×
346
            .collect();
×
347

×
348
        Ok(LayerCollection {
×
349
            id: ProviderLayerCollectionId {
×
350
                provider_id: self.id,
×
351
                collection_id: collection.clone(),
×
352
            },
×
353
            name: "Element 84 AWS STAC".to_owned(),
×
354
            description: "SentinelS2L2ACogs".to_owned(),
×
355
            items,
×
356
            entry_label: None,
×
357
            properties: vec![],
×
358
        })
×
359
    }
×
360

361
    async fn get_root_layer_collection_id(&self) -> Result<LayerCollectionId> {
×
362
        Ok(LayerCollectionId("SentinelS2L2ACogs".to_owned()))
×
363
    }
×
364

365
    async fn load_layer(&self, id: &LayerId) -> Result<Layer> {
×
366
        let dataset = self.datasets.get(id).ok_or(Error::UnknownDataId)?;
×
367

368
        Ok(Layer {
×
369
            id: ProviderLayerId {
×
370
                provider_id: self.id,
×
371
                layer_id: id.clone(),
×
372
            },
×
373
            name: dataset.listing.name.clone(),
×
374
            description: dataset.listing.description.clone(),
×
375
            workflow: Workflow {
×
376
                operator: TypedOperator::Raster(
×
377
                    GdalSource {
×
378
                        params: GdalSourceParameters {
×
379
                            data: NamedData {
×
380
                                namespace: None,
×
381
                                provider: Some(self.id.to_string()),
×
382
                                name: id.to_string(),
×
383
                            },
×
384
                        },
×
385
                    }
×
386
                    .boxed(),
×
387
                ),
×
388
            },
×
389
            symbology: dataset.listing.symbology.clone(),
×
390
            properties: vec![],
×
391
            metadata: HashMap::new(),
×
392
        })
×
393
    }
×
394
}
395

396
#[derive(Debug, Clone)]
397
pub struct SentinelS2L2aCogsMetaData {
398
    api_url: String,
399
    zone: StacZone,
400
    band: StacBand,
401
    stac_api_retries: StacApiRetries,
402
    gdal_retries: GdalRetries,
403
    cache_ttl: CacheTtlSeconds,
404
    stac_query_buffer: StacQueryBuffer,
405
}
406

407
impl SentinelS2L2aCogsMetaData {
408
    #[allow(clippy::too_many_lines)]
409
    async fn create_loading_info(&self, query: RasterQueryRectangle) -> Result<GdalLoadingInfo> {
3✔
410
        // for reference: https://stacspec.org/STAC-ext-api.html#operation/getSearchSTAC
3✔
411
        debug!("create_loading_info with: {:?}", &query);
3✔
412
        let request_params = self.request_params(&query)?;
3✔
413

414
        let query_start_buffer = Duration::seconds(self.stac_query_buffer.start_seconds);
3✔
415
        let query_end_buffer = Duration::seconds(self.stac_query_buffer.end_seconds);
3✔
416

3✔
417
        if request_params.is_none() {
3✔
418
            log::debug!("Request params are empty -> returning empty loading info");
×
419
            return Ok(GdalLoadingInfo::new(
×
420
                // we do not know anything about the data. Can only use query -/+ buffer to determine time bounds.
×
421
                GdalLoadingInfoTemporalSliceIterator::Static {
×
422
                    parts: vec![].into_iter(),
×
423
                },
×
424
                query.time_interval.start() - query_start_buffer,
×
425
                query.time_interval.end() + query_end_buffer,
×
426
            ));
×
427
        }
3✔
428

3✔
429
        let request_params = request_params.expect("The none case was checked above");
3✔
430

3✔
431
        debug!("queried with: {:?}", &request_params);
3✔
432
        let features = self.load_all_features(&request_params).await?;
3✔
433
        debug!("number of features returned by STAC: {}", features.len());
3✔
434
        let mut features: Vec<StacFeature> = features
3✔
435
            .into_iter()
3✔
436
            .filter(|f| {
53✔
437
                f.properties
53✔
438
                    .proj_epsg
53✔
439
                    .is_some_and(|epsg| epsg == self.zone.epsg)
53✔
440
            })
53✔
441
            .collect();
3✔
442

3✔
443
        features.sort_by_key(|a| a.properties.datetime);
612✔
444

3✔
445
        let start_times_pre: Vec<TimeInstance> = features
3✔
446
            .iter()
3✔
447
            .map(|f| TimeInstance::from(f.properties.datetime))
37✔
448
            .collect();
3✔
449
        let start_times = Self::make_unique_start_times_from_sorted_features(&start_times_pre);
3✔
450

3✔
451
        let mut parts: Vec<GdalLoadingInfoTemporalSlice> = vec![];
3✔
452
        let num_features = features.len();
3✔
453
        let mut known_time_start: Option<TimeInstance> = None;
3✔
454
        let mut known_time_end: Option<TimeInstance> = None;
3✔
455
        debug!("number of features in current zone: {}", num_features);
3✔
456
        for i in 0..num_features {
37✔
457
            let feature = &features[i];
37✔
458

37✔
459
            let start = start_times[i];
37✔
460

461
            // feature is valid until next feature starts
462
            let end = if i < num_features - 1 {
37✔
463
                start_times[i + 1]
34✔
464
            } else {
465
                // (or end of query?)
466
                query.time_interval.end() + query_end_buffer
3✔
467
            };
468

469
            /*
470
            Sentinel-2 data are typically acquired around local noon in all UTM zones.
471
            Therefore, a Sentinel-2 image is typically viewed as a time series with daily time steps.
472
            However, the Geo Engine source doesn't yet handle datasets with multiple files per time step.
473
            This requires us to "fake" smaller time steps, so we use the actual capture time of each tile as the start.
474
            To determine the end, we simply assume that the start of the next tile is the end of the previous one.
475
            The first step in using the Sentinel-2 data is usually to aggregate it to a 1-day time series in order to use it in a meaningful way.
476
            An efficient way to aggregate the data to days is to simply use the first valid/last pixel, as there should only be one per day (and overlaps should be the same value).
477
            This start/end time derivation strategy can cause a problem if there is no following tile on the same day: a single tile of day "1" will be valid until a new tile is created on day "1+x".
478
            Firstly, a tile valid for more than one day will appear on days where it is not actually valid.
479
            Secondly, if "first" is used as the aggregation method, an old tile will overlap a new tile starting at noon and therefore the old values will be included in the daily data.
480
            To solve both problems, we limit the validity of each tile to the end of the day on which it was recorded.
481
            This way there is no overlap and the aggregation of the daily data produces valid (expected) results.
482
             */
483
            let end = {
37✔
484
                let start_date = start
37✔
485
                    .as_date_time()
37✔
486
                    .expect("must be a valid date since Sentinel-2 is a very recent thing.");
37✔
487

37✔
488
                let end_date = end
37✔
489
                    .as_date_time()
37✔
490
                    .expect("must be a valid date since Sentinel-2 is a very recent thing.");
37✔
491

37✔
492
                if start_date.year() < end_date.year()
37✔
493
                    || start_date.day_of_year() < end_date.day_of_year()
37✔
494
                {
495
                    TimeInstance::from(
×
496
                        DateTime::new_utc_checked(
×
497
                            start_date.year(),
×
498
                            start_date.month(),
×
499
                            start_date.day(),
×
500
                            0,
×
501
                            0,
×
502
                            0,
×
503
                        )
×
504
                        .expect("Must be a valid date since it is already valid."),
×
505
                    ) + Duration::days(1)
×
506
                } else {
507
                    end
37✔
508
                }
509
            };
510

511
            let time_interval = TimeInterval::new(start, end)?;
37✔
512

513
            if time_interval.start() <= query.time_interval.start() {
37✔
514
                let t = if time_interval.end() > query.time_interval.start() {
17✔
515
                    time_interval.start()
3✔
516
                } else {
517
                    time_interval.end()
14✔
518
                };
519
                known_time_start = known_time_start.map(|old| old.max(t)).or(Some(t));
17✔
520
            }
20✔
521

522
            if time_interval.end() >= query.time_interval.end() {
37✔
523
                let t = if time_interval.start() < query.time_interval.end() {
25✔
524
                    time_interval.end()
2✔
525
                } else {
526
                    time_interval.start()
23✔
527
                };
528
                known_time_end = known_time_end.map(|old| old.min(t)).or(Some(t));
25✔
529
            }
25✔
530

531
            if time_interval.intersects(&query.time_interval) {
37✔
532
                debug!(
3✔
533
                    "STAC asset time: {}, url: {}",
×
534
                    time_interval,
×
535
                    feature
×
536
                        .assets
×
537
                        .get(&self.band.name)
×
538
                        .map_or(&"n/a".to_string(), |a| &a.href)
×
539
                );
540

541
                let asset =
3✔
542
                    feature
3✔
543
                        .assets
3✔
544
                        .get(&self.band.name)
3✔
545
                        .ok_or(error::Error::StacNoSuchBand {
3✔
546
                            band_name: self.band.name.clone(),
3✔
547
                        })?;
3✔
548

549
                parts.push(self.create_loading_info_part(time_interval, asset, self.cache_ttl)?);
3✔
550
            }
34✔
551
        }
552
        debug!("number of generated loading infos: {}", parts.len());
3✔
553

554
        // if there is no information of time outside the query, we fallback to the only information we know: query -/+ buffer. We also use that information if we did not find a better time
555
        let query_start = query.time_interval.start() - query_start_buffer;
3✔
556
        let known_time_before = known_time_start.unwrap_or(query_start).min(query_start);
3✔
557

3✔
558
        let query_end = query.time_interval.end() + query_end_buffer;
3✔
559
        let known_time_after = known_time_end.unwrap_or(query_end).max(query_end);
3✔
560

3✔
561
        Ok(GdalLoadingInfo::new(
3✔
562
            GdalLoadingInfoTemporalSliceIterator::Static {
3✔
563
                parts: parts.into_iter(),
3✔
564
            },
3✔
565
            known_time_before,
3✔
566
            known_time_after,
3✔
567
        ))
3✔
568
    }
3✔
569

570
    fn make_unique_start_times_from_sorted_features(
7✔
571
        start_times: &[TimeInstance],
7✔
572
    ) -> Vec<TimeInstance> {
7✔
573
        let mut unique_start_times: Vec<TimeInstance> = Vec::with_capacity(start_times.len());
7✔
574
        for (i, &t_start) in start_times.iter().enumerate() {
61✔
575
            let real_start = if i == 0 {
61✔
576
                t_start
7✔
577
            } else {
578
                let prev_start = start_times[i - 1];
54✔
579
                if t_start == prev_start {
54✔
580
                    let prev_u_start = unique_start_times[i - 1];
6✔
581
                    let new_u_start = prev_u_start + 1;
6✔
582
                    log::debug!(
6✔
583
                        "duplicate start time: {} insert as {} following {}",
×
584
                        t_start.as_datetime_string(),
×
585
                        new_u_start.as_datetime_string(),
×
586
                        prev_u_start.as_datetime_string()
×
587
                    );
588
                    new_u_start
6✔
589
                } else {
590
                    t_start
48✔
591
                }
592
            };
593

594
            unique_start_times.push(real_start);
61✔
595
        }
596
        unique_start_times
7✔
597
    }
7✔
598

599
    fn create_loading_info_part(
3✔
600
        &self,
3✔
601
        time_interval: TimeInterval,
3✔
602
        asset: &StacAsset,
3✔
603
        cache_ttl: CacheTtlSeconds,
3✔
604
    ) -> Result<GdalLoadingInfoTemporalSlice> {
3✔
605
        let [stac_shape_y, stac_shape_x] = asset.proj_shape.ok_or(error::Error::StacInvalidBbox)?;
3✔
606

607
        Ok(GdalLoadingInfoTemporalSlice {
608
            time: time_interval,
3✔
609
            params: Some(GdalDatasetParameters {
3✔
610
                file_path: PathBuf::from(format!("/vsicurl/{}", asset.href)),
3✔
611
                rasterband_channel: 1,
3✔
612
                geo_transform: GdalDatasetGeoTransform::from(
3✔
613
                    asset
3✔
614
                        .gdal_geotransform()
3✔
615
                        .ok_or(error::Error::StacInvalidGeoTransform)?,
3✔
616
                ),
617
                width: stac_shape_x as usize,
3✔
618
                height: stac_shape_y as usize,
3✔
619
                file_not_found_handling: geoengine_operators::source::FileNotFoundHandling::NoData,
3✔
620
                no_data_value: self.band.no_data_value,
3✔
621
                properties_mapping: None,
3✔
622
                gdal_open_options: None,
3✔
623
                gdal_config_options: Some(vec![
3✔
624
                    // only read the tif file and no aux files, etc.
3✔
625
                    (
3✔
626
                        "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_string(),
3✔
627
                        ".tif".to_string(),
3✔
628
                    ),
3✔
629
                    // do not perform a directory scan on the AWS bucket
3✔
630
                    (
3✔
631
                        "GDAL_DISABLE_READDIR_ON_OPEN".to_string(),
3✔
632
                        "EMPTY_DIR".to_string(),
3✔
633
                    ),
3✔
634
                    // do not try to read credentials from home directory
3✔
635
                    ("GDAL_HTTP_NETRC".to_string(), "NO".to_string()),
3✔
636
                    // disable Gdal's retry because geo engine does its own retry
3✔
637
                    ("GDAL_HTTP_MAX_RETRY".to_string(), "0".to_string()),
3✔
638
                ]),
3✔
639
                allow_alphaband_as_mask: true,
3✔
640
                retry: Some(GdalRetryOptions {
3✔
641
                    max_retries: self.gdal_retries.number_of_retries,
3✔
642
                }),
3✔
643
            }),
3✔
644
            cache_ttl,
3✔
645
        })
646
    }
3✔
647

648
    fn request_params(
3✔
649
        &self,
3✔
650
        query: &RasterQueryRectangle,
3✔
651
    ) -> Result<Option<Vec<(String, String)>>> {
3✔
652
        let (t_start, t_end) = Self::time_range_request(&query.time_interval)?;
3✔
653

654
        let t_start = t_start - Duration::seconds(self.stac_query_buffer.start_seconds);
3✔
655
        let t_end = t_end + Duration::seconds(self.stac_query_buffer.end_seconds);
3✔
656

657
        // request all features in zone in order to be able to determine the temporal validity of individual tile
658
        let projector = CoordinateProjector::from_known_srs(
3✔
659
            SpatialReference::new(SpatialReferenceAuthority::Epsg, self.zone.epsg),
3✔
660
            SpatialReference::epsg_4326(),
3✔
661
        )?;
3✔
662

663
        let spatial_partition = query.spatial_partition(); // TODO: use SpatialPartition2D directly
3✔
664
        let bbox = BoundingBox2D::new_upper_left_lower_right_unchecked(
3✔
665
            spatial_partition.upper_left(),
3✔
666
            spatial_partition.lower_right(),
3✔
667
        );
3✔
668
        let bbox = bbox.reproject_clipped(&projector)?; // TODO: use reproject_clipped on SpatialPartition2D
3✔
669

670
        Ok(bbox.map(|bbox| {
3✔
671
            vec![
3✔
672
                (
3✔
673
                    "collections[]".to_owned(),
3✔
674
                    "sentinel-s2-l2a-cogs".to_owned(),
3✔
675
                ),
3✔
676
                (
3✔
677
                    "bbox".to_owned(),
3✔
678
                    format!(
3✔
679
                        "[{},{},{},{}]", // array-brackets are not used in standard but required here for unknkown reason
3✔
680
                        bbox.lower_left().x,
3✔
681
                        bbox.lower_left().y,
3✔
682
                        bbox.upper_right().x,
3✔
683
                        bbox.upper_right().y
3✔
684
                    ),
3✔
685
                ), // TODO: order coordinates depending on projection
3✔
686
                (
3✔
687
                    "datetime".to_owned(),
3✔
688
                    format!(
3✔
689
                        "{}/{}",
3✔
690
                        t_start.to_datetime_string(),
3✔
691
                        t_end.to_datetime_string()
3✔
692
                    ),
3✔
693
                ),
3✔
694
                ("limit".to_owned(), "500".to_owned()),
3✔
695
            ]
3✔
696
        }))
3✔
697
    }
3✔
698

699
    async fn load_all_features<T: Serialize + ?Sized + Debug>(
3✔
700
        &self,
3✔
701
        params: &T,
3✔
702
    ) -> Result<Vec<StacFeature>> {
3✔
703
        let mut features = vec![];
3✔
704

705
        let mut collection = self.load_collection(params, 1).await?;
3✔
706
        features.append(&mut collection.features);
3✔
707

3✔
708
        let num_pages =
3✔
709
            (collection.context.matched as f64 / collection.context.limit as f64).ceil() as u32;
3✔
710

711
        for page in 2..=num_pages {
3✔
712
            let mut collection = self.load_collection(params, page).await?;
×
713
            features.append(&mut collection.features);
×
714
        }
715

716
        Ok(features)
3✔
717
    }
3✔
718

719
    async fn load_collection<T: Serialize + ?Sized + Debug>(
3✔
720
        &self,
3✔
721
        params: &T,
3✔
722
        page: u32,
3✔
723
    ) -> Result<StacCollection> {
3✔
724
        let client = Client::builder().build()?;
3✔
725

726
        retry(
3✔
727
            self.stac_api_retries.number_of_retries,
3✔
728
            self.stac_api_retries.initial_delay_ms,
3✔
729
            self.stac_api_retries.exponential_backoff_factor,
3✔
730
            Some(STAC_RETRY_MAX_BACKOFF_MS),
3✔
731
            || async {
4✔
732
                let text = client
4✔
733
                    .get(&self.api_url)
4✔
734
                    .query(&params)
4✔
735
                    .query(&[("page", &page.to_string())])
4✔
736
                    .send()
4✔
737
                    .await
4✔
738
                    .context(error::Reqwest)?
4✔
739
                    .text()
4✔
740
                    .await
4✔
741
                    .context(error::Reqwest)?;
4✔
742

743
                serde_json::from_str::<StacCollection>(&text).map_err(|error| {
4✔
744
                    error::Error::StacJsonResponse {
1✔
745
                        url: self.api_url.clone(),
1✔
746
                        response: text,
1✔
747
                        error,
1✔
748
                    }
1✔
749
                })
4✔
750
            },
8✔
751
        )
3✔
752
        .await
3✔
753
    }
3✔
754

755
    fn time_range_request(time: &TimeInterval) -> Result<(DateTime, DateTime)> {
3✔
756
        let t_start =
3✔
757
            time.start()
3✔
758
                .as_date_time()
3✔
759
                .ok_or(geoengine_operators::error::Error::DataType {
3✔
760
                    source: geoengine_datatypes::error::Error::NoDateTimeValid {
3✔
761
                        time_instance: time.start(),
3✔
762
                    },
3✔
763
                })?;
3✔
764

765
        let t_end =
3✔
766
            time.end()
3✔
767
                .as_date_time()
3✔
768
                .ok_or(geoengine_operators::error::Error::DataType {
3✔
769
                    source: geoengine_datatypes::error::Error::NoDateTimeValid {
3✔
770
                        time_instance: time.end(),
3✔
771
                    },
3✔
772
                })?;
3✔
773

774
        Ok((t_start, t_end))
3✔
775
    }
3✔
776
}
777

778
#[async_trait]
779
impl MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
780
    for SentinelS2L2aCogsMetaData
781
{
782
    async fn loading_info(
783
        &self,
784
        query: RasterQueryRectangle,
785
    ) -> geoengine_operators::util::Result<GdalLoadingInfo> {
3✔
786
        // TODO: propagate error properly
787
        debug!("loading_info for: {:?}", &query);
3✔
788
        self.create_loading_info(query).await.map_err(|e| {
3✔
789
            geoengine_operators::error::Error::LoadingInfo {
×
790
                source: Box::new(e),
×
791
            }
×
792
        })
3✔
793
    }
6✔
794

795
    async fn result_descriptor(&self) -> geoengine_operators::util::Result<RasterResultDescriptor> {
2✔
796
        Ok(RasterResultDescriptor {
2✔
797
            data_type: self.band.data_type,
2✔
798
            spatial_reference: SpatialReference::new(
2✔
799
                SpatialReferenceAuthority::Epsg,
2✔
800
                self.zone.epsg,
2✔
801
            )
2✔
802
            .into(),
2✔
803
            time: None,
2✔
804
            bbox: None,
2✔
805
            resolution: None, // TODO: determine from STAC or data or hardcode it
2✔
806
            bands: RasterBandDescriptors::new_single_band(),
2✔
807
        })
2✔
808
    }
4✔
809

810
    fn box_clone(
2✔
811
        &self,
2✔
812
    ) -> Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> {
2✔
813
        Box::new(self.clone())
2✔
814
    }
2✔
815
}
816

817
#[async_trait]
818
impl MetaDataProvider<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
819
    for SentinelS2L2aCogsDataProvider
820
{
821
    async fn meta_data(
822
        &self,
823
        id: &geoengine_datatypes::dataset::DataId,
824
    ) -> Result<
825
        Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>>,
826
        geoengine_operators::error::Error,
827
    > {
3✔
828
        let id: DataId = id.clone();
3✔
829

830
        let dataset = self
3✔
831
            .datasets
3✔
832
            .get(
3✔
833
                &id.external()
3✔
834
                    .ok_or(geoengine_operators::error::Error::LoadingInfo {
3✔
835
                        source: Box::new(error::Error::DataIdTypeMissMatch),
3✔
836
                    })?
3✔
837
                    .layer_id,
838
            )
839
            .ok_or(geoengine_operators::error::Error::UnknownDataId)?;
3✔
840

841
        Ok(Box::new(SentinelS2L2aCogsMetaData {
3✔
842
            api_url: self.api_url.clone(),
3✔
843
            zone: dataset.zone.clone(),
3✔
844
            band: dataset.band.clone(),
3✔
845
            stac_api_retries: self.stac_api_retries,
3✔
846
            gdal_retries: self.gdal_retries,
3✔
847
            cache_ttl: self.cache_ttl,
3✔
848
            stac_query_buffer: self.query_buffer,
3✔
849
        }))
3✔
850
    }
6✔
851
}
852

853
#[async_trait]
854
impl
855
    MetaDataProvider<MockDatasetDataSourceLoadingInfo, VectorResultDescriptor, VectorQueryRectangle>
856
    for SentinelS2L2aCogsDataProvider
857
{
858
    async fn meta_data(
859
        &self,
860
        _id: &geoengine_datatypes::dataset::DataId,
861
    ) -> Result<
862
        Box<
863
            dyn MetaData<
864
                MockDatasetDataSourceLoadingInfo,
865
                VectorResultDescriptor,
866
                VectorQueryRectangle,
867
            >,
868
        >,
869
        geoengine_operators::error::Error,
870
    > {
×
871
        Err(geoengine_operators::error::Error::NotImplemented)
×
872
    }
×
873
}
874

875
#[async_trait]
876
impl MetaDataProvider<OgrSourceDataset, VectorResultDescriptor, VectorQueryRectangle>
877
    for SentinelS2L2aCogsDataProvider
878
{
879
    async fn meta_data(
880
        &self,
881
        _id: &geoengine_datatypes::dataset::DataId,
882
    ) -> Result<
883
        Box<dyn MetaData<OgrSourceDataset, VectorResultDescriptor, VectorQueryRectangle>>,
884
        geoengine_operators::error::Error,
885
    > {
×
886
        Err(geoengine_operators::error::Error::NotImplemented)
×
887
    }
×
888
}
889

890
#[cfg(test)]
891
mod tests {
892
    use super::*;
893
    use crate::{
894
        contexts::{ApplicationContext, SessionContext},
895
        contexts::{PostgresContext, PostgresDb},
896
        ge_context,
897
        layers::storage::{LayerProviderDb, LayerProviderListing, LayerProviderListingOptions},
898
        test_data,
899
        users::UserAuth,
900
        util::tests::admin_login,
901
    };
902
    use futures::StreamExt;
903
    use geoengine_datatypes::{
904
        dataset::{DatasetId, ExternalDataId},
905
        primitives::{BandSelection, SpatialPartition2D, SpatialResolution},
906
        util::{gdal::hide_gdal_errors, test::TestDefault, Identifier},
907
    };
908
    use geoengine_operators::{
909
        engine::{
910
            ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
911
            WorkflowOperatorPath,
912
        },
913
        source::{FileNotFoundHandling, GdalMetaDataStatic, GdalSource, GdalSourceParameters},
914
    };
915
    use httptest::{
916
        all_of,
917
        matchers::{contains, request, url_decoded},
918
        responders::{self},
919
        Expectation, Server,
920
    };
921
    use std::{fs::File, io::BufReader, str::FromStr};
922
    use tokio_postgres::NoTls;
923

924
    #[ge_context::test]
1✔
925
    async fn loading_info(app_ctx: PostgresContext<NoTls>) -> Result<()> {
1✔
926
        // TODO: mock STAC endpoint
927

928
        let def: SentinelS2L2ACogsProviderDefinition = serde_json::from_reader(BufReader::new(
1✔
929
            File::open(test_data!("provider_defs/sentinel_s2_l2a_cogs.json"))?,
1✔
930
        ))?;
×
931

932
        let provider = Box::new(def)
1✔
933
            .initialize(
1✔
934
                app_ctx
1✔
935
                    .session_context(app_ctx.create_anonymous_session().await?)
1✔
936
                    .db(),
1✔
937
            )
1✔
938
            .await?;
1✔
939

940
        let meta: Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> =
1✔
941
            provider
1✔
942
                .meta_data(
1✔
943
                    &ExternalDataId {
1✔
944
                        provider_id: DataProviderId::from_str(
1✔
945
                            "5779494c-f3a2-48b3-8a2d-5fbba8c5b6c5",
1✔
946
                        )?,
1✔
947
                        layer_id: LayerId("UTM32N:B01".to_owned()),
1✔
948
                    }
1✔
949
                    .into(),
1✔
950
                )
1✔
951
                .await
1✔
952
                .unwrap();
1✔
953

954
        let loading_info = meta
1✔
955
            .loading_info(RasterQueryRectangle {
1✔
956
                spatial_bounds: SpatialPartition2D::new(
1✔
957
                    (166_021.44, 9_329_005.18).into(),
1✔
958
                    (534_994.66, 0.00).into(),
1✔
959
                )
1✔
960
                .unwrap(),
1✔
961
                time_interval: TimeInterval::new_instant(DateTime::new_utc(2021, 1, 2, 10, 2, 26))?,
1✔
962
                spatial_resolution: SpatialResolution::one(),
1✔
963
                attributes: BandSelection::first(),
1✔
964
            })
1✔
965
            .await
1✔
966
            .unwrap();
1✔
967

1✔
968
        let expected = vec![GdalLoadingInfoTemporalSlice {
1✔
969
            time: TimeInterval::new_unchecked(1_609_581_746_000, 1_609_581_758_000),
1✔
970
            params: Some(GdalDatasetParameters {
1✔
971
                file_path: "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/32/R/PU/2021/1/S2B_32RPU_20210102_0_L2A/B01.tif".into(),
1✔
972
                rasterband_channel: 1,
1✔
973
                geo_transform: GdalDatasetGeoTransform {
1✔
974
                    origin_coordinate: (600_000.0, 3_400_020.0).into(),
1✔
975
                    x_pixel_size: 60.,
1✔
976
                    y_pixel_size: -60.,
1✔
977
                },
1✔
978
                width: 1830,
1✔
979
                height: 1830,
1✔
980
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
981
                no_data_value: Some(0.),
1✔
982
                properties_mapping: None,
1✔
983
                gdal_open_options: None,
1✔
984
                gdal_config_options: Some(vec![
1✔
985
                    ("CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(), ".tif".to_owned()),
1✔
986
                    ("GDAL_DISABLE_READDIR_ON_OPEN".to_owned(), "EMPTY_DIR".to_owned()),
1✔
987
                    ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
988
                    ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_owned())
1✔
989
                    ]),
1✔
990
                allow_alphaband_as_mask: true,
1✔
991
                retry: Some(GdalRetryOptions { max_retries: 10 }),
1✔
992
            }),
1✔
993
            cache_ttl: CacheTtlSeconds::new(86_400),
1✔
994
        }];
1✔
995

996
        if let GdalLoadingInfoTemporalSliceIterator::Static { parts } = loading_info.info {
1✔
997
            let result: Vec<_> = parts.collect();
1✔
998

1✔
999
            assert_eq!(result.len(), 1);
1✔
1000

1001
            assert_eq!(result, expected);
1✔
1002
        } else {
1003
            unreachable!();
×
1004
        }
1005

1006
        Ok(())
1✔
1007
    }
1✔
1008

1009
    #[ge_context::test]
1✔
1010
    async fn query_data(app_ctx: PostgresContext<NoTls>) -> Result<()> {
1✔
1011
        // TODO: mock STAC endpoint
1✔
1012

1✔
1013
        let mut exe = MockExecutionContext::test_default();
1✔
1014

1015
        let def: SentinelS2L2ACogsProviderDefinition = serde_json::from_reader(BufReader::new(
1✔
1016
            File::open(test_data!("provider_defs/sentinel_s2_l2a_cogs.json"))?,
1✔
1017
        ))?;
×
1018

1019
        let provider = Box::new(def)
1✔
1020
            .initialize(
1✔
1021
                app_ctx
1✔
1022
                    .session_context(app_ctx.create_anonymous_session().await?)
1✔
1023
                    .db(),
1✔
1024
            )
1✔
1025
            .await?;
1✔
1026

1027
        let meta: Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> =
1✔
1028
            provider
1✔
1029
                .meta_data(
1✔
1030
                    &ExternalDataId {
1✔
1031
                        provider_id: DataProviderId::from_str(
1✔
1032
                            "5779494c-f3a2-48b3-8a2d-5fbba8c5b6c5",
1✔
1033
                        )?,
1✔
1034
                        layer_id: LayerId("UTM32N:B01".to_owned()),
1✔
1035
                    }
1✔
1036
                    .into(),
1✔
1037
                )
1✔
1038
                .await?;
1✔
1039

1040
        let name = NamedData {
1✔
1041
            namespace: None,
1✔
1042
            provider: Some("5779494c-f3a2-48b3-8a2d-5fbba8c5b6c5".into()),
1✔
1043
            name: "UTM32N-B01".into(),
1✔
1044
        };
1✔
1045

1✔
1046
        exe.add_meta_data(
1✔
1047
            ExternalDataId {
1✔
1048
                provider_id: DataProviderId::from_str("5779494c-f3a2-48b3-8a2d-5fbba8c5b6c5")?,
1✔
1049
                layer_id: LayerId("UTM32N:B01".to_owned()),
1✔
1050
            }
1✔
1051
            .into(),
1✔
1052
            name.clone(),
1✔
1053
            meta,
1✔
1054
        );
1055

1056
        let op = GdalSource {
1✔
1057
            params: GdalSourceParameters { data: name },
1✔
1058
        }
1✔
1059
        .boxed()
1✔
1060
        .initialize(WorkflowOperatorPath::initialize_root(), &exe)
1✔
1061
        .await
1✔
1062
        .unwrap();
1✔
1063

1064
        let processor = op.query_processor()?.get_u16().unwrap();
1✔
1065

1✔
1066
        let spatial_bounds =
1✔
1067
            SpatialPartition2D::new((166_021.44, 9_329_005.18).into(), (534_994.66, 0.00).into())
1✔
1068
                .unwrap();
1✔
1069

1✔
1070
        let spatial_resolution = SpatialResolution::new_unchecked(
1✔
1071
            spatial_bounds.size_x() / 256.,
1✔
1072
            spatial_bounds.size_y() / 256.,
1✔
1073
        );
1✔
1074
        let query = RasterQueryRectangle {
1✔
1075
            spatial_bounds,
1✔
1076
            time_interval: TimeInterval::new_instant(DateTime::new_utc(2021, 1, 2, 10, 2, 26))?,
1✔
1077
            spatial_resolution,
1✔
1078
            attributes: BandSelection::first(),
1✔
1079
        };
1✔
1080

1✔
1081
        let ctx = MockQueryContext::new(ChunkByteSize::MAX);
1✔
1082

1083
        let result = processor
1✔
1084
            .raster_query(query, &ctx)
1✔
1085
            .await?
1✔
1086
            .collect::<Vec<_>>()
1✔
1087
            .await;
1✔
1088

1089
        // TODO: check actual data
1090
        assert_eq!(result.len(), 1);
1✔
1091

1092
        Ok(())
1✔
1093
    }
1✔
1094

1095
    #[ge_context::test]
1✔
1096
    #[allow(clippy::too_many_lines)]
1097
    async fn query_data_with_failing_requests(app_ctx: PostgresContext<NoTls>) {
1✔
1098
        // crate::util::tests::initialize_debugging_in_test(); // use for debugging
1✔
1099
        hide_gdal_errors();
1✔
1100

1✔
1101
        let stac_response =
1✔
1102
            std::fs::read_to_string(test_data!("stac_responses/items_page_1_limit_500.json"))
1✔
1103
                .unwrap();
1✔
1104
        let server = Server::run();
1✔
1105

1✔
1106
        // STAC response
1✔
1107
        server.expect(
1✔
1108
            Expectation::matching(all_of![
1✔
1109
                request::method_path("GET", "/v0/collections/sentinel-s2-l2a-cogs/items",),
1✔
1110
                request::query(url_decoded(contains((
1✔
1111
                    "collections[]",
1✔
1112
                    "sentinel-s2-l2a-cogs"
1✔
1113
                )))),
1✔
1114
                request::query(url_decoded(contains(("page", "1")))),
1✔
1115
                request::query(url_decoded(contains(("limit", "500")))),
1✔
1116
                request::query(url_decoded(contains((
1✔
1117
                    "bbox",
1✔
1118
                    "[33.899332958586406,-2.261536424319933,33.900232774450984,-2.2606312588790414]"
1✔
1119
                )))),
1✔
1120
                request::query(url_decoded(contains((
1✔
1121
                    "datetime",
1✔
1122
                    // default case adds one minute to the start/end of the query to catch elements before/after 
1✔
1123
                    "2021-09-23T08:09:44+00:00/2021-09-23T08:11:44+00:00"
1✔
1124
                )))),
1✔
1125
            ])
1✔
1126
            .times(2)
1✔
1127
            .respond_with(responders::cycle![
1✔
1128
                // first fail
1✔
1129
                responders::status_code(404),
1✔
1130
                // then succeed
1✔
1131
                responders::status_code(200)
1✔
1132
                    .append_header("Content-Type", "application/json")
1✔
1133
                    .body(stac_response),
1✔
1134
            ]),
1✔
1135
        );
1✔
1136

1✔
1137
        // HEAD request
1✔
1138
        let head_success_response = || {
5✔
1139
            responders::status_code(200)
5✔
1140
                .append_header(
5✔
1141
                    "x-amz-id-2",
5✔
1142
                    "avRd0/ks4ATH99UNXBCfqZAEQ3BckuLJTj7iG1jQrGoxOtwswqHrok10u+VMHO3twVIhUmQKLwg=",
5✔
1143
                )
5✔
1144
                .append_header("x-amz-request-id", "VVHWX1P45NP7KNWV")
5✔
1145
                .append_header("Date", "Tue, 11 Oct 2022 16:06:03 GMT")
5✔
1146
                .append_header("Last-Modified", "Fri, 09 Sep 2022 00:32:25 GMT")
5✔
1147
                .append_header("ETag", "\"09a4c36021930e67dd1c71ed303cdf4e-24\"")
5✔
1148
                .append_header("Cache-Control", "public, max-age=31536000, immutable")
5✔
1149
                .append_header("Accept-Ranges", "bytes")
5✔
1150
                .append_header(
5✔
1151
                    "Content-Type",
5✔
1152
                    "image/tiff; application=geotiff; profile=cloud-optimized",
5✔
1153
                )
5✔
1154
                .append_header("Server", "AmazonS3")
5✔
1155
                .append_header("Content-Length", "197770048")
5✔
1156
        };
5✔
1157

1158
        server.expect(
1✔
1159
            Expectation::matching(request::method_path(
1✔
1160
                "HEAD",
1✔
1161
                "/sentinel-s2-l2a-cogs/36/M/WC/2021/9/S2B_36MWC_20210923_0_L2A/B04.tif",
1✔
1162
            ))
1✔
1163
            .times(7)
1✔
1164
            .respond_with(responders::cycle![
1✔
1165
                // first fail
1✔
1166
                responders::status_code(500),
1✔
1167
                // then time out
1✔
1168
                responders::delay_and_then(
1✔
1169
                    std::time::Duration::from_secs(2),
1✔
1170
                    responders::status_code(500)
1✔
1171
                ),
1✔
1172
                // then succeed
1✔
1173
                head_success_response(), // -> GET COG header fails
1✔
1174
                head_success_response(), // -> GET COG header times out
1✔
1175
                head_success_response(), // -> GET COG header succeeds, GET tile fails
1✔
1176
                head_success_response(), // -> GET COG header succeeds, GET tile times out
1✔
1177
                head_success_response(), // -> GET COG header succeeds, GET tile IReadBlock failed
1✔
1178
            ]),
1✔
1179
        );
1✔
1180

1✔
1181
        // GET request to read contents of COG header
1✔
1182
        let get_success_response = || {
3✔
1183
            responders::status_code(206)
3✔
1184
                .append_header("Content-Type", "application/json")
3✔
1185
                .body(
3✔
1186
                    include_bytes!("../../../../test_data/stac_responses/cog-header.bin").to_vec(),
3✔
1187
                )
3✔
1188
                .append_header(
3✔
1189
                    "x-amz-id-2",
3✔
1190
                    "avRd0/ks4ATH99UNXBCfqZAEQ3BckuLJTj7iG1jQrGoxOtwswqHrok10u+VMHO3twVIhUmQKLwg=",
3✔
1191
                )
3✔
1192
                .append_header("x-amz-request-id", "VVHWX1P45NP7KNWV")
3✔
1193
                .append_header("Date", "Tue, 11 Oct 2022 16:06:03 GMT")
3✔
1194
                .append_header("Last-Modified", "Fri, 09 Sep 2022 00:32:25 GMT")
3✔
1195
                .append_header("ETag", "\"09a4c36021930e67dd1c71ed303cdf4e-24\"")
3✔
1196
                .append_header("Cache-Control", "public, max-age=31536000, immutable")
3✔
1197
                .append_header("Accept-Ranges", "bytes")
3✔
1198
                .append_header("Content-Range", "bytes 0-16383/197770048")
3✔
1199
                .append_header(
3✔
1200
                    "Content-Type",
3✔
1201
                    "image/tiff; application=geotiff; profile=cloud-optimized",
3✔
1202
                )
3✔
1203
                .append_header("Server", "AmazonS3")
3✔
1204
                .append_header("Content-Length", "16384")
3✔
1205
        };
3✔
1206

1207
        server.expect(
1✔
1208
            Expectation::matching(all_of![
1✔
1209
                request::method_path(
1✔
1210
                    "GET",
1✔
1211
                    "/sentinel-s2-l2a-cogs/36/M/WC/2021/9/S2B_36MWC_20210923_0_L2A/B04.tif",
1✔
1212
                ),
1✔
1213
                request::headers(contains(("range", "bytes=0-16383"))),
1✔
1214
            ])
1✔
1215
            .times(5)
1✔
1216
            .respond_with(responders::cycle![
1✔
1217
                // first fail
1✔
1218
                responders::status_code(500),
1✔
1219
                // then time out
1✔
1220
                responders::delay_and_then(
1✔
1221
                    std::time::Duration::from_secs(2),
1✔
1222
                    responders::status_code(500)
1✔
1223
                ),
1✔
1224
                // then succeed
1✔
1225
                get_success_response(), // -> GET tile fails
1✔
1226
                get_success_response(), // -> GET tile times out
1✔
1227
                get_success_response(), // -> GET tile times IReadBlock failed
1✔
1228
            ]),
1✔
1229
        );
1✔
1230

1✔
1231
        // GET request of the COG tile
1✔
1232
        server.expect(
1✔
1233
            Expectation::matching(all_of![
1✔
1234
                request::method_path(
1✔
1235
                    "GET",
1✔
1236
                    "/sentinel-s2-l2a-cogs/36/M/WC/2021/9/S2B_36MWC_20210923_0_L2A/B04.tif",
1✔
1237
                ),
1✔
1238
                request::headers(contains(("range", "bytes=46170112-46186495"))),
1✔
1239
            ])
1✔
1240
            .times(4)
1✔
1241
            .respond_with(responders::cycle![
1✔
1242
                // first fail
1✔
1243
                responders::status_code(500),
1✔
1244
                // then time out
1✔
1245
                responders::delay_and_then(
1✔
1246
                    std::time::Duration::from_secs(2),
1✔
1247
                    responders::status_code(500)
1✔
1248
                ),
1✔
1249
                // then return incomplete tile (to force error "band 1: IReadBlock failed at X offset 0, Y offset 0: TIFFReadEncodedTile() failed.")
1✔
1250
                responders::status_code(206)
1✔
1251
                    .append_header("Content-Type", "application/json")
1✔
1252
                    .body(
1✔
1253
                        include_bytes!(
1✔
1254
                            "../../../../test_data/stac_responses/cog-tile.bin"
1✔
1255
                        )[0..2]
1✔
1256
                        .to_vec()
1✔
1257
                    ).append_header(
1✔
1258
                        "x-amz-id-2",
1✔
1259
                        "avRd0/ks4ATH99UNXBCfqZAEQ3BckuLJTj7iG1jQrGoxOtwswqHrok10u+VMHO3twVIhUmQKLwg=",
1✔
1260
                    )
1✔
1261
                    .append_header("x-amz-request-id", "VVHWX1P45NP7KNWV")
1✔
1262
                    .append_header("Date", "Tue, 11 Oct 2022 16:06:03 GMT")
1✔
1263
                    .append_header("Last-Modified", "Fri, 09 Sep 2022 00:32:25 GMT")
1✔
1264
                    .append_header("ETag", "\"09a4c36021930e67dd1c71ed303cdf4e-24\"")
1✔
1265
                    .append_header("Cache-Control", "public, max-age=31536000, immutable")
1✔
1266
                    .append_header("Accept-Ranges", "bytes")
1✔
1267
                    .append_header("Content-Range", "bytes 46170112-46170113/173560205")
1✔
1268
                    .append_header(
1✔
1269
                        "Content-Type",
1✔
1270
                        "image/tiff; application=geotiff; profile=cloud-optimized",
1✔
1271
                    )
1✔
1272
                    .append_header("Server", "AmazonS3")
1✔
1273
                    .append_header("Content-Length", "2"),
1✔
1274
                 // then succeed
1✔
1275
                responders::status_code(206)
1✔
1276
                    .append_header("Content-Type", "application/json")
1✔
1277
                    .body(
1✔
1278
                        include_bytes!(
1✔
1279
                            "../../../../test_data/stac_responses/cog-tile.bin"
1✔
1280
                        )
1✔
1281
                        .to_vec()
1✔
1282
                    ).append_header(
1✔
1283
                        "x-amz-id-2",
1✔
1284
                        "avRd0/ks4ATH99UNXBCfqZAEQ3BckuLJTj7iG1jQrGoxOtwswqHrok10u+VMHO3twVIhUmQKLwg=",
1✔
1285
                    )
1✔
1286
                    .append_header("x-amz-request-id", "VVHWX1P45NP7KNWV")
1✔
1287
                    .append_header("Date", "Tue, 11 Oct 2022 16:06:03 GMT")
1✔
1288
                    .append_header("Last-Modified", "Fri, 09 Sep 2022 00:32:25 GMT")
1✔
1289
                    .append_header("ETag", "\"09a4c36021930e67dd1c71ed303cdf4e-24\"")
1✔
1290
                    .append_header("Cache-Control", "public, max-age=31536000, immutable")
1✔
1291
                    .append_header("Accept-Ranges", "bytes")
1✔
1292
                    .append_header("Content-Range", "bytes 46170112-46186495/173560205")
1✔
1293
                    .append_header(
1✔
1294
                        "Content-Type",
1✔
1295
                        "image/tiff; application=geotiff; profile=cloud-optimized",
1✔
1296
                    )
1✔
1297
                    .append_header("Server", "AmazonS3")
1✔
1298
                    .append_header("Content-Length", "16384"),
1✔
1299
            ]),
1✔
1300
        );
1✔
1301

1✔
1302
        let provider_id: DataProviderId = "5779494c-f3a2-48b3-8a2d-5fbba8c5b6c5".parse().unwrap();
1✔
1303

1✔
1304
        let provider_def: Box<dyn DataProviderDefinition<PostgresDb<NoTls>>> =
1✔
1305
            Box::new(SentinelS2L2ACogsProviderDefinition {
1✔
1306
                name: "Element 84 AWS STAC".into(),
1✔
1307
                id: provider_id,
1✔
1308
                description: "Access to Sentinel 2 L2A COGs on AWS".into(),
1✔
1309
                priority: Some(22),
1✔
1310
                api_url: server.url_str("/v0/collections/sentinel-s2-l2a-cogs/items"),
1✔
1311
                bands: vec![StacBand {
1✔
1312
                    name: "B04".into(),
1✔
1313
                    no_data_value: Some(0.),
1✔
1314
                    data_type: RasterDataType::U16,
1✔
1315
                }],
1✔
1316
                zones: vec![StacZone {
1✔
1317
                    name: "UTM36S".into(),
1✔
1318
                    epsg: 32736,
1✔
1319
                }],
1✔
1320
                stac_api_retries: Default::default(),
1✔
1321
                gdal_retries: GdalRetries {
1✔
1322
                    number_of_retries: 999,
1✔
1323
                },
1✔
1324
                cache_ttl: Default::default(),
1✔
1325
                query_buffer: Default::default(),
1✔
1326
            });
1✔
1327

1328
        let provider = provider_def
1✔
1329
            .initialize(
1✔
1330
                app_ctx
1✔
1331
                    .session_context(app_ctx.create_anonymous_session().await.unwrap())
1✔
1332
                    .db(),
1✔
1333
            )
1✔
1334
            .await
1✔
1335
            .unwrap();
1✔
1336

1337
        let meta: Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>> =
1✔
1338
            provider
1✔
1339
                .meta_data(
1✔
1340
                    &ExternalDataId {
1✔
1341
                        provider_id,
1✔
1342
                        layer_id: LayerId("UTM36S:B04".to_owned()),
1✔
1343
                    }
1✔
1344
                    .into(),
1✔
1345
                )
1✔
1346
                .await
1✔
1347
                .unwrap();
1✔
1348

1✔
1349
        let query = RasterQueryRectangle {
1✔
1350
            spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
1351
                (600_000.00, 9_750_100.).into(),
1✔
1352
                (600_100.0, 9_750_000.).into(),
1✔
1353
            ),
1✔
1354
            time_interval: TimeInterval::new_instant(DateTime::new_utc(2021, 9, 23, 8, 10, 44))
1✔
1355
                .unwrap(),
1✔
1356
            spatial_resolution: SpatialResolution::new_unchecked(10., 10.),
1✔
1357
            attributes: BandSelection::first(),
1✔
1358
        };
1✔
1359

1360
        let loading_info = meta.loading_info(query).await.unwrap();
1✔
1361
        let parts =
1✔
1362
            if let GdalLoadingInfoTemporalSliceIterator::Static { parts } = loading_info.info {
1✔
1363
                parts.collect::<Vec<_>>()
1✔
1364
            } else {
1365
                panic!("expected static parts");
×
1366
            };
1367

1368
        assert_eq!(
1✔
1369
            parts,
1✔
1370
            vec![GdalLoadingInfoTemporalSlice {
1✔
1371
                time: TimeInterval::new_unchecked(1_632_384_644_000,1_632_384_704_000),
1✔
1372
                params: Some(GdalDatasetParameters {
1✔
1373
                    file_path: "/vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/M/WC/2021/9/S2B_36MWC_20210923_0_L2A/B04.tif".into(),
1✔
1374
                    rasterband_channel: 1,
1✔
1375
                    geo_transform: GdalDatasetGeoTransform {
1✔
1376
                        origin_coordinate: (499_980.0,9_800_020.00).into(),
1✔
1377
                        x_pixel_size: 10.,
1✔
1378
                        y_pixel_size: -10.,
1✔
1379
                    },
1✔
1380
                    width: 10980,
1✔
1381
                    height: 10980,
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: Some(vec![
1✔
1387
                        ("CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(), ".tif".to_owned()),
1✔
1388
                        ("GDAL_DISABLE_READDIR_ON_OPEN".to_owned(), "EMPTY_DIR".to_owned()),
1✔
1389
                        ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
1390
                        ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
1391
                        ]),
1✔
1392
                    allow_alphaband_as_mask: true,
1✔
1393
                    retry: Some(GdalRetryOptions { max_retries: 999 }),
1✔
1394
                }),
1✔
1395
                cache_ttl: CacheTtlSeconds::default(),
1✔
1396
            }]
1✔
1397
        );
1✔
1398

1399
        let mut params = parts[0].clone().params.unwrap();
1✔
1400
        params.file_path = params
1✔
1401
            .file_path
1✔
1402
            .to_str()
1✔
1403
            .unwrap()
1✔
1404
            .replace(
1✔
1405
                "https://sentinel-cogs.s3.us-west-2.amazonaws.com/",
1✔
1406
                &server.url_str(""),
1✔
1407
            )
1✔
1408
            .into();
1✔
1409
        // add a low `GDAL_HTTP_TIMEOUT` value to test timeouts
1✔
1410
        params.gdal_config_options = params.gdal_config_options.map(|mut options| {
1✔
1411
            options.push(("GDAL_HTTP_TIMEOUT".to_owned(), "1".to_owned()));
1✔
1412
            options
1✔
1413
        });
1✔
1414

1✔
1415
        let mut execution_context = MockExecutionContext::test_default();
1✔
1416
        let id: geoengine_datatypes::dataset::DataId = DatasetId::new().into();
1✔
1417
        let name = NamedData {
1✔
1418
            namespace: None,
1✔
1419
            provider: None,
1✔
1420
            name: "UTM36S-B04".into(),
1✔
1421
        };
1✔
1422
        execution_context.add_meta_data(
1✔
1423
            id.clone(),
1✔
1424
            name.clone(),
1✔
1425
            Box::new(GdalMetaDataStatic {
1✔
1426
                time: None,
1✔
1427
                result_descriptor: RasterResultDescriptor {
1✔
1428
                    data_type: RasterDataType::U16,
1✔
1429
                    spatial_reference: SpatialReference::from_str("EPSG:32736").unwrap().into(),
1✔
1430
                    time: None,
1✔
1431
                    bbox: None,
1✔
1432
                    resolution: None,
1✔
1433
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1434
                },
1✔
1435
                params,
1✔
1436
                cache_ttl: CacheTtlSeconds::default(),
1✔
1437
            }),
1✔
1438
        );
1✔
1439

1440
        let gdal_source = GdalSource {
1✔
1441
            params: GdalSourceParameters { data: name },
1✔
1442
        }
1✔
1443
        .boxed()
1✔
1444
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1445
        .await
1✔
1446
        .unwrap()
1✔
1447
        .query_processor()
1✔
1448
        .unwrap()
1✔
1449
        .get_u16()
1✔
1450
        .unwrap();
1✔
1451

1✔
1452
        let query_context = MockQueryContext::test_default();
1✔
1453

1454
        let stream = gdal_source
1✔
1455
            .raster_query(
1✔
1456
                RasterQueryRectangle {
1✔
1457
                    spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
1458
                        (499_980., 9_804_800.).into(),
1✔
1459
                        (499_990., 9_804_810.).into(),
1✔
1460
                    ),
1✔
1461
                    time_interval: TimeInterval::new_instant(DateTime::new_utc(
1✔
1462
                        2014, 3, 1, 0, 0, 0,
1✔
1463
                    ))
1✔
1464
                    .unwrap(),
1✔
1465
                    spatial_resolution: SpatialResolution::new(10., 10.).unwrap(),
1✔
1466
                    attributes: BandSelection::first(),
1✔
1467
                },
1✔
1468
                &query_context,
1✔
1469
            )
1✔
1470
            .await
1✔
1471
            .unwrap();
1✔
1472

1473
        let result = stream.collect::<Vec<_>>().await;
1✔
1474

1475
        assert_eq!(result.len(), 1);
1✔
1476
        assert!(result[0].is_ok());
1✔
1477
    }
1✔
1478

1479
    #[test]
1480
    fn make_unique_timestamps_no_dups() {
1✔
1481
        let timestamps = vec![
1✔
1482
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1483
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1484
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1485
            TimeInstance::from_millis(1_632_384_647_000).unwrap(),
1✔
1486
            TimeInstance::from_millis(1_632_384_648_000).unwrap(),
1✔
1487
            TimeInstance::from_millis(1_632_384_649_000).unwrap(),
1✔
1488
        ];
1✔
1489

1✔
1490
        let uts =
1✔
1491
            SentinelS2L2aCogsMetaData::make_unique_start_times_from_sorted_features(&timestamps);
1✔
1492

1✔
1493
        assert_eq!(uts, timestamps);
1✔
1494
    }
1✔
1495

1496
    #[test]
1497
    fn make_unique_timestamps_two_dups() {
1✔
1498
        let timestamps = vec![
1✔
1499
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1500
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1501
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1502
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1503
            TimeInstance::from_millis(1_632_384_647_000).unwrap(),
1✔
1504
            TimeInstance::from_millis(1_632_384_648_000).unwrap(),
1✔
1505
        ];
1✔
1506

1✔
1507
        let uts =
1✔
1508
            SentinelS2L2aCogsMetaData::make_unique_start_times_from_sorted_features(&timestamps);
1✔
1509

1✔
1510
        let expected_timestamps = vec![
1✔
1511
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1512
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1513
            TimeInstance::from_millis(1_632_384_645_001).unwrap(),
1✔
1514
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1515
            TimeInstance::from_millis(1_632_384_647_000).unwrap(),
1✔
1516
            TimeInstance::from_millis(1_632_384_648_000).unwrap(),
1✔
1517
        ];
1✔
1518

1✔
1519
        assert_eq!(uts, expected_timestamps);
1✔
1520
    }
1✔
1521

1522
    #[test]
1523
    fn make_unique_timestamps_three_dups() {
1✔
1524
        let timestamps = vec![
1✔
1525
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1526
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1527
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1528
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1529
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1530
            TimeInstance::from_millis(1_632_384_647_000).unwrap(),
1✔
1531
        ];
1✔
1532

1✔
1533
        let uts =
1✔
1534
            SentinelS2L2aCogsMetaData::make_unique_start_times_from_sorted_features(&timestamps);
1✔
1535

1✔
1536
        let expected_timestamps = vec![
1✔
1537
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1538
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1539
            TimeInstance::from_millis(1_632_384_645_001).unwrap(),
1✔
1540
            TimeInstance::from_millis(1_632_384_645_002).unwrap(),
1✔
1541
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1542
            TimeInstance::from_millis(1_632_384_647_000).unwrap(),
1✔
1543
        ];
1✔
1544

1✔
1545
        assert_eq!(uts, expected_timestamps);
1✔
1546
    }
1✔
1547

1548
    #[test]
1549
    fn make_unique_timestamps_four_dups() {
1✔
1550
        let timestamps = vec![
1✔
1551
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1552
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1553
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1554
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1555
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1556
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1557
        ];
1✔
1558

1✔
1559
        let uts =
1✔
1560
            SentinelS2L2aCogsMetaData::make_unique_start_times_from_sorted_features(&timestamps);
1✔
1561

1✔
1562
        let expected_timestamps = vec![
1✔
1563
            TimeInstance::from_millis(1_632_384_644_000).unwrap(),
1✔
1564
            TimeInstance::from_millis(1_632_384_645_000).unwrap(),
1✔
1565
            TimeInstance::from_millis(1_632_384_645_001).unwrap(),
1✔
1566
            TimeInstance::from_millis(1_632_384_645_002).unwrap(),
1✔
1567
            TimeInstance::from_millis(1_632_384_645_003).unwrap(),
1✔
1568
            TimeInstance::from_millis(1_632_384_646_000).unwrap(),
1✔
1569
        ];
1✔
1570

1✔
1571
        assert_eq!(uts, expected_timestamps);
1✔
1572
    }
1✔
1573

1574
    #[ge_context::test]
1✔
1575
    async fn it_adds_the_data_provider_to_the_db(app_ctx: PostgresContext<NoTls>) {
1✔
1576
        let session = admin_login(&app_ctx).await;
1✔
1577
        let ctx = app_ctx.session_context(session.clone());
1✔
1578

1✔
1579
        let def: SentinelS2L2ACogsProviderDefinition = serde_json::from_reader(BufReader::new(
1✔
1580
            File::open(test_data!("provider_defs/sentinel_s2_l2a_cogs.json")).unwrap(),
1✔
1581
        ))
1✔
1582
        .unwrap();
1✔
1583

1✔
1584
        ctx.db().add_layer_provider(def.into()).await.unwrap();
1✔
1585

1✔
1586
        ctx.db()
1✔
1587
            .load_layer_provider(DataProviderId::from_u128(
1✔
1588
                0x5779494c_f3a2_48b3_8a2d_5fbba8c5b6c5,
1✔
1589
            ))
1✔
1590
            .await
1✔
1591
            .unwrap();
1✔
1592

1593
        let providers = ctx
1✔
1594
            .db()
1✔
1595
            .list_layer_providers(LayerProviderListingOptions {
1✔
1596
                offset: 0,
1✔
1597
                limit: 2,
1✔
1598
            })
1✔
1599
            .await
1✔
1600
            .unwrap();
1✔
1601

1✔
1602
        assert_eq!(providers.len(), 1);
1✔
1603

1604
        assert_eq!(
1✔
1605
            providers[0],
1✔
1606
            LayerProviderListing {
1✔
1607
                id: DataProviderId::from_u128(0x5779494c_f3a2_48b3_8a2d_5fbba8c5b6c5),
1✔
1608
                name: "Element 84 AWS STAC".to_owned(),
1✔
1609
                priority: 50,
1✔
1610
            }
1✔
1611
        );
1✔
1612
    }
1✔
1613
}
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