• 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

86.43
/services/src/datasets/external/edr.rs
1
use crate::contexts::GeoEngineDb;
2
use crate::datasets::listing::{Provenance, ProvenanceOutput};
3
use crate::error::{Error, Result};
4
use crate::layers::external::{DataProvider, DataProviderDefinition};
5
use crate::layers::layer::{
6
    CollectionItem, Layer, LayerCollection, LayerCollectionListOptions, LayerCollectionListing,
7
    LayerListing, ProviderLayerCollectionId, ProviderLayerId,
8
};
9
use crate::layers::listing::{
10
    LayerCollectionId, LayerCollectionProvider, ProviderCapabilities, SearchCapabilities,
11
};
12
use crate::util::parsing::deserialize_base_url;
13
use crate::workflows::workflow::Workflow;
14
use async_trait::async_trait;
15
use gdal::Dataset;
16
use geoengine_datatypes::collections::VectorDataType;
17
use geoengine_datatypes::dataset::{DataId, DataProviderId, LayerId};
18
use geoengine_datatypes::hashmap;
19
use geoengine_datatypes::primitives::{
20
    AxisAlignedRectangle, BoundingBox2D, CacheTtlSeconds, ContinuousMeasurement, Coordinate2D,
21
    FeatureDataType, Measurement, RasterQueryRectangle, SpatialPartition2D, TimeInstance,
22
    TimeInterval, VectorQueryRectangle,
23
};
24
use geoengine_datatypes::raster::RasterDataType;
25
use geoengine_datatypes::spatial_reference::SpatialReference;
26
use geoengine_operators::engine::{
27
    MetaData, MetaDataProvider, RasterBandDescriptors, RasterOperator, RasterResultDescriptor,
28
    StaticMetaData, TypedOperator, VectorColumnInfo, VectorOperator, VectorResultDescriptor,
29
};
30
use geoengine_operators::mock::MockDatasetDataSourceLoadingInfo;
31
use geoengine_operators::source::{
32
    FileNotFoundHandling, GdalDatasetParameters, GdalLoadingInfo, GdalLoadingInfoTemporalSlice,
33
    GdalMetaDataList, GdalSource, GdalSourceParameters, OgrSource, OgrSourceColumnSpec,
34
    OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec,
35
    OgrSourceParameters, OgrSourceTimeFormat,
36
};
37
use geoengine_operators::util::gdal::gdal_open_dataset;
38
use geoengine_operators::util::TemporaryGdalThreadLocalConfigOptions;
39
use reqwest::Client;
40
use serde::{Deserialize, Serialize};
41
use snafu::prelude::*;
42
use std::collections::{BTreeMap, HashMap};
43
use std::str::FromStr;
44
use std::sync::OnceLock;
45
use url::Url;
46

47
static IS_FILETYPE_RASTER: OnceLock<HashMap<&'static str, bool>> = OnceLock::new();
48

49
// TODO: change to `LazyLock' once stable
50
fn init_is_filetype_raster() -> HashMap<&'static str, bool> {
1✔
51
    //name:is_raster
1✔
52
    hashmap! {
1✔
53
        "GeoTIFF" => true,
1✔
54
        "GeoJSON" => false
1✔
55
    }
1✔
56
}
1✔
57

58
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
×
59
#[serde(rename_all = "camelCase")]
60
pub struct EdrDataProviderDefinition {
61
    pub name: String,
62
    pub description: String,
63
    pub priority: Option<i16>,
64
    pub id: DataProviderId,
65
    #[serde(deserialize_with = "deserialize_base_url")]
66
    pub base_url: Url,
67
    pub vector_spec: Option<EdrVectorSpec>,
68
    #[serde(default)]
69
    pub cache_ttl: CacheTtlSeconds,
70
    #[serde(default)]
71
    /// List of vertical reference systems with a discrete scale
72
    pub discrete_vrs: Vec<String>,
73
    pub provenance: Option<Vec<Provenance>>,
74
}
75

76
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77
pub struct EdrVectorSpec {
78
    pub x: String,
79
    pub y: Option<String>,
80
    pub time: String,
81
}
82

83
#[async_trait]
84
impl<D: GeoEngineDb> DataProviderDefinition<D> for EdrDataProviderDefinition {
85
    async fn initialize(self: Box<Self>, _db: D) -> Result<Box<dyn DataProvider>> {
10✔
86
        Ok(Box::new(EdrDataProvider {
10✔
87
            id: self.id,
10✔
88
            name: self.name,
10✔
89
            description: self.description,
10✔
90
            base_url: self.base_url,
10✔
91
            vector_spec: self.vector_spec,
10✔
92
            client: Client::new(),
10✔
93
            cache_ttl: self.cache_ttl,
10✔
94
            discrete_vrs: self.discrete_vrs,
10✔
95
            provenance: self.provenance,
10✔
96
        }))
10✔
97
    }
20✔
98

99
    fn type_name(&self) -> &'static str {
×
100
        "Environmental Data Retrieval"
×
101
    }
×
102

103
    fn name(&self) -> String {
×
104
        self.name.clone()
×
105
    }
×
106

107
    fn id(&self) -> DataProviderId {
×
108
        self.id
×
109
    }
×
110

111
    fn priority(&self) -> i16 {
×
112
        self.priority.unwrap_or(0)
×
113
    }
×
114
}
115

116
#[derive(Debug)]
117
pub struct EdrDataProvider {
118
    id: DataProviderId,
119
    name: String,
120
    description: String,
121
    base_url: Url,
122
    vector_spec: Option<EdrVectorSpec>,
123
    client: Client,
124
    cache_ttl: CacheTtlSeconds,
125
    /// List of vertical reference systems with a discrete scale
126
    discrete_vrs: Vec<String>,
127
    provenance: Option<Vec<Provenance>>,
128
}
129

130
#[async_trait]
131
impl DataProvider for EdrDataProvider {
132
    async fn provenance(&self, id: &DataId) -> Result<ProvenanceOutput> {
×
133
        Ok(ProvenanceOutput {
×
134
            data: id.clone(),
×
135
            provenance: self.provenance.clone(),
×
136
        })
×
137
    }
×
138
}
139

140
impl EdrDataProvider {
141
    async fn load_collection_by_name(
8✔
142
        &self,
8✔
143
        collection_name: &str,
8✔
144
    ) -> Result<EdrCollectionMetaData> {
8✔
145
        self.client
8✔
146
            .get(
8✔
147
                self.base_url
8✔
148
                    .join(&format!("collections/{collection_name}?f=json"))?,
8✔
149
            )
150
            .send()
8✔
151
            .await?
8✔
152
            .json()
8✔
153
            .await
8✔
154
            .map_err(|_| Error::EdrInvalidMetadataFormat)
8✔
155
    }
8✔
156

157
    async fn load_collection_by_dataid(
2✔
158
        &self,
2✔
159
        id: &geoengine_datatypes::dataset::DataId,
2✔
160
    ) -> Result<(EdrCollectionId, EdrCollectionMetaData), geoengine_operators::error::Error> {
2✔
161
        let layer_id = id
2✔
162
            .external()
2✔
163
            .ok_or(Error::InvalidDataId)
2✔
164
            .map_err(|e| geoengine_operators::error::Error::LoadingInfo {
2✔
165
                source: Box::new(e),
×
166
            })?
2✔
167
            .layer_id;
168
        let edr_id: EdrCollectionId = EdrCollectionId::from_str(&layer_id.0).map_err(|e| {
2✔
169
            geoengine_operators::error::Error::LoadingInfo {
×
170
                source: Box::new(e),
×
171
            }
×
172
        })?;
2✔
173
        let collection_name = edr_id.get_collection_id().map_err(|e| {
2✔
174
            geoengine_operators::error::Error::LoadingInfo {
×
175
                source: Box::new(e),
×
176
            }
×
177
        })?;
2✔
178
        let collection_meta: EdrCollectionMetaData = self
2✔
179
            .load_collection_by_name(collection_name)
2✔
180
            .await
2✔
181
            .map_err(|e| geoengine_operators::error::Error::LoadingInfo {
2✔
182
                source: Box::new(e),
×
183
            })?;
2✔
184
        Ok((edr_id, collection_meta))
2✔
185
    }
2✔
186

187
    async fn get_root_collection(
1✔
188
        &self,
1✔
189
        collection_id: &LayerCollectionId,
1✔
190
        options: &LayerCollectionListOptions,
1✔
191
    ) -> Result<LayerCollection> {
1✔
192
        let collections: EdrCollectionsMetaData = self
1✔
193
            .client
1✔
194
            .get(self.base_url.join("collections?f=json")?)
1✔
195
            .send()
1✔
196
            .await?
1✔
197
            .json()
1✔
198
            .await
1✔
199
            .map_err(|_| Error::EdrInvalidMetadataFormat)?;
1✔
200

201
        let items = collections
1✔
202
            .collections
1✔
203
            .into_iter()
1✔
204
            .filter(|collection| {
6✔
205
                collection.data_queries.cube.is_some() && collection.extent.spatial.is_some()
6✔
206
            })
6✔
207
            .skip(options.offset as usize)
1✔
208
            .take(options.limit as usize)
1✔
209
            .map(|collection| {
5✔
210
                if collection.is_raster_file()? || collection.extent.vertical.is_some() {
5✔
211
                    Ok(CollectionItem::Collection(LayerCollectionListing {
212
                        r#type: Default::default(),
4✔
213
                        id: ProviderLayerCollectionId {
4✔
214
                            provider_id: self.id,
4✔
215
                            collection_id: EdrCollectionId::Collection {
4✔
216
                                collection: collection.id.clone(),
4✔
217
                            }
4✔
218
                            .try_into()?,
4✔
219
                        },
220
                        name: collection.title.unwrap_or(collection.id),
4✔
221
                        description: collection.description.unwrap_or_default(),
4✔
222
                        properties: vec![],
4✔
223
                    }))
224
                } else {
225
                    Ok(CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
226
                        id: ProviderLayerId {
1✔
227
                            provider_id: self.id,
1✔
228
                            layer_id: EdrCollectionId::Collection {
1✔
229
                                collection: collection.id.clone(),
1✔
230
                            }
1✔
231
                            .try_into()?,
1✔
232
                        },
233
                        name: collection.title.unwrap_or(collection.id),
1✔
234
                        description: collection.description.unwrap_or_default(),
1✔
235
                        properties: vec![],
1✔
236
                    }))
237
                }
238
            })
5✔
239
            .collect::<Result<Vec<CollectionItem>>>()?;
1✔
240

241
        Ok(LayerCollection {
1✔
242
            id: ProviderLayerCollectionId {
1✔
243
                provider_id: self.id,
1✔
244
                collection_id: collection_id.clone(),
1✔
245
            },
1✔
246
            name: "EDR".to_owned(),
1✔
247
            description: "Environmental Data Retrieval".to_owned(),
1✔
248
            items,
1✔
249
            entry_label: None,
1✔
250
            properties: vec![],
1✔
251
        })
1✔
252
    }
1✔
253

254
    fn get_raster_parameter_collection(
1✔
255
        &self,
1✔
256
        collection_id: &LayerCollectionId,
1✔
257
        collection_meta: EdrCollectionMetaData,
1✔
258
        options: &LayerCollectionListOptions,
1✔
259
    ) -> Result<LayerCollection> {
1✔
260
        let items = collection_meta
1✔
261
            .parameter_names
1✔
262
            .into_keys()
1✔
263
            .skip(options.offset as usize)
1✔
264
            .take(options.limit as usize)
1✔
265
            .map(|parameter_name| {
1✔
266
                if collection_meta.extent.vertical.is_some() {
1✔
267
                    Ok(CollectionItem::Collection(LayerCollectionListing {
268
                        r#type: Default::default(),
1✔
269
                        id: ProviderLayerCollectionId {
1✔
270
                            provider_id: self.id,
1✔
271
                            collection_id: EdrCollectionId::ParameterOrHeight {
1✔
272
                                collection: collection_meta.id.clone(),
1✔
273
                                parameter: parameter_name.clone(),
1✔
274
                            }
1✔
275
                            .try_into()?,
1✔
276
                        },
277
                        name: parameter_name,
1✔
278
                        description: String::new(),
1✔
279
                        properties: vec![],
1✔
280
                    }))
281
                } else {
NEW
282
                    Ok(CollectionItem::Layer(LayerListing { r#type: Default::default(),
×
UNCOV
283
                        id: ProviderLayerId {
×
284
                            provider_id: self.id,
×
285
                            layer_id: EdrCollectionId::ParameterOrHeight {
×
286
                                collection: collection_meta.id.clone(),
×
287
                                parameter: parameter_name.clone(),
×
288
                            }
×
289
                            .try_into()?,
×
290
                        },
291
                        name: parameter_name,
×
292
                        description: String::new(),
×
293
                        properties: vec![],
×
294
                    }))
295
                }
296
            })
1✔
297
            .collect::<Result<Vec<CollectionItem>>>()?;
1✔
298

299
        Ok(LayerCollection {
1✔
300
            id: ProviderLayerCollectionId {
1✔
301
                provider_id: self.id,
1✔
302
                collection_id: collection_id.clone(),
1✔
303
            },
1✔
304
            name: collection_meta.id.clone(),
1✔
305
            description: format!("Parameters of {}", collection_meta.id),
1✔
306
            items,
1✔
307
            entry_label: None,
1✔
308
            properties: vec![],
1✔
309
        })
1✔
310
    }
1✔
311

312
    fn get_vector_height_collection(
1✔
313
        &self,
1✔
314
        collection_id: &LayerCollectionId,
1✔
315
        collection_meta: EdrCollectionMetaData,
1✔
316
        options: &LayerCollectionListOptions,
1✔
317
    ) -> Result<LayerCollection> {
1✔
318
        let items = collection_meta
1✔
319
            .extent
1✔
320
            .vertical
1✔
321
            .expect("checked before")
1✔
322
            .values
1✔
323
            .into_iter()
1✔
324
            .skip(options.offset as usize)
1✔
325
            .take(options.limit as usize)
1✔
326
            .map(|height| {
2✔
327
                Ok(CollectionItem::Layer(LayerListing { r#type: Default::default(),
2✔
328
                    id: ProviderLayerId {
2✔
329
                        provider_id: self.id,
2✔
330
                        layer_id: EdrCollectionId::ParameterOrHeight {
2✔
331
                            collection: collection_meta.id.clone(),
2✔
332
                            parameter: height.clone(),
2✔
333
                        }
2✔
334
                        .try_into()?,
2✔
335
                    },
336
                    name: height,
2✔
337
                    description: String::new(),
2✔
338
                    properties: vec![],
2✔
339
                }))
340
            })
2✔
341
            .collect::<Result<Vec<CollectionItem>>>()?;
1✔
342

343
        Ok(LayerCollection {
1✔
344
            id: ProviderLayerCollectionId {
1✔
345
                provider_id: self.id,
1✔
346
                collection_id: collection_id.clone(),
1✔
347
            },
1✔
348
            name: collection_meta.id.clone(),
1✔
349
            description: format!("Height selection of {}", collection_meta.id),
1✔
350
            items,
1✔
351
            entry_label: None,
1✔
352
            properties: vec![],
1✔
353
        })
1✔
354
    }
1✔
355

356
    fn get_raster_height_collection(
1✔
357
        &self,
1✔
358
        collection_id: &LayerCollectionId,
1✔
359
        collection_meta: EdrCollectionMetaData,
1✔
360
        parameter: &str,
1✔
361
        options: &LayerCollectionListOptions,
1✔
362
    ) -> Result<LayerCollection> {
1✔
363
        let items = collection_meta
1✔
364
            .extent
1✔
365
            .vertical
1✔
366
            .expect("checked before")
1✔
367
            .values
1✔
368
            .into_iter()
1✔
369
            .skip(options.offset as usize)
1✔
370
            .take(options.limit as usize)
1✔
371
            .map(|height| {
2✔
372
                Ok(CollectionItem::Layer(LayerListing { r#type: Default::default(),
2✔
373
                    id: ProviderLayerId {
2✔
374
                        provider_id: self.id,
2✔
375
                        layer_id: EdrCollectionId::ParameterAndHeight {
2✔
376
                            collection: collection_meta.id.clone(),
2✔
377
                            parameter: parameter.to_string(),
2✔
378
                            height: height.clone(),
2✔
379
                        }
2✔
380
                        .try_into()?,
2✔
381
                    },
382
                    name: height,
2✔
383
                    description: String::new(),
2✔
384
                    properties: vec![],
2✔
385
                }))
386
            })
2✔
387
            .collect::<Result<Vec<CollectionItem>>>()?;
1✔
388

389
        Ok(LayerCollection {
1✔
390
            id: ProviderLayerCollectionId {
1✔
391
                provider_id: self.id,
1✔
392
                collection_id: collection_id.clone(),
1✔
393
            },
1✔
394
            name: collection_meta.id.clone(),
1✔
395
            description: format!("Height selection of {}", collection_meta.id),
1✔
396
            items,
1✔
397
            entry_label: None,
1✔
398
            properties: vec![],
1✔
399
        })
1✔
400
    }
1✔
401
}
402

403
#[derive(Deserialize)]
404
struct EdrCollectionsMetaData {
405
    collections: Vec<EdrCollectionMetaData>,
406
}
407

408
#[derive(Deserialize)]
409
struct EdrCollectionMetaData {
410
    id: String,
411
    title: Option<String>,
412
    description: Option<String>,
413
    extent: EdrExtents,
414
    //for paging keys need to be returned in same order every time
415
    parameter_names: BTreeMap<String, EdrParameter>,
416
    output_formats: Vec<String>,
417
    data_queries: EdrDataQueries,
418
}
419

420
#[derive(Deserialize)]
421
struct EdrDataQueries {
422
    cube: Option<serde_json::Value>,
423
}
424

425
impl EdrCollectionMetaData {
426
    fn get_time_interval(&self) -> Result<TimeInterval, geoengine_operators::error::Error> {
2✔
427
        let temporal_extent = self.extent.temporal.as_ref().ok_or_else(|| {
2✔
428
            geoengine_operators::error::Error::DatasetMetaData {
×
429
                source: Box::new(EdrProviderError::MissingTemporalExtent),
×
430
            }
×
431
        })?;
2✔
432

433
        time_interval_from_strings(
2✔
434
            &temporal_extent.interval[0][0],
2✔
435
            &temporal_extent.interval[0][1],
2✔
436
        )
2✔
437
    }
2✔
438

439
    fn get_bounding_box(&self) -> Result<BoundingBox2D, geoengine_operators::error::Error> {
2✔
440
        let spatial_extent = self.extent.spatial.as_ref().ok_or_else(|| {
2✔
441
            geoengine_operators::error::Error::DatasetMetaData {
×
442
                source: Box::new(EdrProviderError::MissingSpatialExtent),
×
443
            }
×
444
        })?;
2✔
445

446
        Ok(BoundingBox2D::new_unchecked(
2✔
447
            Coordinate2D::new(spatial_extent.bbox[0][0], spatial_extent.bbox[0][1]),
2✔
448
            Coordinate2D::new(spatial_extent.bbox[0][2], spatial_extent.bbox[0][3]),
2✔
449
        ))
2✔
450
    }
2✔
451

452
    fn select_output_format(&self) -> Result<String, geoengine_operators::error::Error> {
15✔
453
        for format in &self.output_formats {
51✔
454
            if IS_FILETYPE_RASTER
51✔
455
                .get_or_init(init_is_filetype_raster)
51✔
456
                .contains_key(format.as_str())
51✔
457
            {
458
                return Ok(format.to_string());
15✔
459
            }
36✔
460
        }
461
        Err(geoengine_operators::error::Error::DatasetMetaData {
×
462
            source: Box::new(EdrProviderError::NoSupportedOutputFormat),
×
463
        })
×
464
    }
15✔
465

466
    fn is_raster_file(&self) -> Result<bool, geoengine_operators::error::Error> {
11✔
467
        Ok(*IS_FILETYPE_RASTER
11✔
468
            .get_or_init(init_is_filetype_raster)
11✔
469
            .get(&self.select_output_format()?.as_str())
11✔
470
            .expect("can only return values in map"))
11✔
471
    }
11✔
472

473
    fn get_vector_download_url(
1✔
474
        &self,
1✔
475
        base_url: &Url,
1✔
476
        height: &str,
1✔
477
        discrete_vrs: &[String],
1✔
478
    ) -> Result<(String, String), geoengine_operators::error::Error> {
1✔
479
        let spatial_extent = self.extent.spatial.as_ref().ok_or_else(|| {
1✔
480
            geoengine_operators::error::Error::DatasetMetaData {
×
481
                source: Box::new(EdrProviderError::MissingSpatialExtent),
×
482
            }
×
483
        })?;
1✔
484
        let temporal_extent = self.extent.temporal.as_ref().ok_or_else(|| {
1✔
485
            geoengine_operators::error::Error::DatasetMetaData {
×
486
                source: Box::new(EdrProviderError::MissingTemporalExtent),
×
487
            }
×
488
        })?;
1✔
489
        let z = if height == "default" {
1✔
490
            String::new()
1✔
491
        } else if self.extent.has_discrete_vertical_axis(discrete_vrs) {
×
492
            format!("&z={height}")
×
493
        } else {
494
            format!("&z={height}%2F{height}")
×
495
        };
496
        let layer_name = format!(
1✔
497
            "cube?bbox={},{},{},{}{}&datetime={}%2F{}&f={}",
1✔
498
            spatial_extent.bbox[0][0],
1✔
499
            spatial_extent.bbox[0][1],
1✔
500
            spatial_extent.bbox[0][2],
1✔
501
            spatial_extent.bbox[0][3],
1✔
502
            z,
1✔
503
            temporal_extent.interval[0][0],
1✔
504
            temporal_extent.interval[0][1],
1✔
505
            self.select_output_format()?
1✔
506
        );
507
        let download_url = format!(
1✔
508
            "/vsicurl_streaming/{}collections/{}/{}",
1✔
509
            base_url, self.id, layer_name,
1✔
510
        );
1✔
511
        Ok((download_url, layer_name))
1✔
512
    }
1✔
513

514
    fn get_raster_download_url(
3✔
515
        &self,
3✔
516
        base_url: &Url,
3✔
517
        parameter_name: &str,
3✔
518
        height: &str,
3✔
519
        time: &str,
3✔
520
        discrete_vrs: &[String],
3✔
521
    ) -> Result<String, geoengine_operators::error::Error> {
3✔
522
        let spatial_extent = self.extent.spatial.as_ref().ok_or_else(|| {
3✔
523
            geoengine_operators::error::Error::DatasetMetaData {
×
524
                source: Box::new(EdrProviderError::MissingSpatialExtent),
×
525
            }
×
526
        })?;
3✔
527
        let z = if height == "default" {
3✔
528
            String::new()
×
529
        } else if self.extent.has_discrete_vertical_axis(discrete_vrs) {
3✔
530
            format!("&z={height}")
×
531
        } else {
532
            format!("&z={height}%2F{height}")
3✔
533
        };
534
        Ok(format!(
3✔
535
            "/vsicurl_streaming/{}collections/{}/cube?bbox={},{},{},{}{}&datetime={}%2F{}&f={}&parameter-name={}",
3✔
536
            base_url,
3✔
537
            self.id,
3✔
538
            spatial_extent.bbox[0][0],
3✔
539
            spatial_extent.bbox[0][1],
3✔
540
            spatial_extent.bbox[0][2],
3✔
541
            spatial_extent.bbox[0][3],
3✔
542
            z,
3✔
543
            time,
3✔
544
            time,
3✔
545
            self.select_output_format()?,
3✔
546
            parameter_name
547
        ))
548
    }
3✔
549

550
    fn get_vector_result_descriptor(
1✔
551
        &self,
1✔
552
    ) -> Result<VectorResultDescriptor, geoengine_operators::error::Error> {
1✔
553
        let column_map: HashMap<String, VectorColumnInfo> = self
1✔
554
            .parameter_names
1✔
555
            .iter()
1✔
556
            .map(|(parameter_name, parameter_metadata)| {
1✔
557
                let data_type = if let Some(data_type) = parameter_metadata.data_type.as_ref() {
1✔
558
                    data_type.as_str().to_uppercase()
1✔
559
                } else {
560
                    "FLOAT".to_string()
×
561
                };
562
                match data_type.as_str() {
1✔
563
                    "STRING" => (
1✔
564
                        parameter_name.to_string(),
×
565
                        VectorColumnInfo {
×
566
                            data_type: FeatureDataType::Text,
×
567
                            measurement: Measurement::Unitless,
×
568
                        },
×
569
                    ),
×
570
                    "INTEGER" => (
1✔
571
                        parameter_name.to_string(),
1✔
572
                        VectorColumnInfo {
1✔
573
                            data_type: FeatureDataType::Int,
1✔
574
                            measurement: Measurement::Continuous(ContinuousMeasurement {
1✔
575
                                measurement: parameter_metadata.observed_property.label.clone(),
1✔
576
                                unit: parameter_metadata.unit.as_ref().map(|x| x.symbol.clone()),
1✔
577
                            }),
1✔
578
                        },
1✔
579
                    ),
1✔
580
                    _ => (
×
581
                        parameter_name.to_string(),
×
582
                        VectorColumnInfo {
×
583
                            data_type: FeatureDataType::Float,
×
584
                            measurement: Measurement::Continuous(ContinuousMeasurement {
×
585
                                measurement: parameter_metadata.observed_property.label.clone(),
×
586
                                unit: parameter_metadata.unit.as_ref().map(|x| x.symbol.clone()),
×
587
                            }),
×
588
                        },
×
589
                    ),
×
590
                }
591
            })
1✔
592
            .collect();
1✔
593

1✔
594
        Ok(VectorResultDescriptor {
1✔
595
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
596
            data_type: VectorDataType::MultiPoint,
1✔
597
            columns: column_map,
1✔
598
            time: Some(self.get_time_interval()?),
1✔
599
            bbox: Some(self.get_bounding_box()?),
1✔
600
        })
601
    }
1✔
602

603
    fn get_column_spec(&self, vector_spec: EdrVectorSpec) -> OgrSourceColumnSpec {
1✔
604
        let mut int = vec![];
1✔
605
        let mut float = vec![];
1✔
606
        let mut text = vec![];
1✔
607
        let bool = vec![];
1✔
608
        let datetime = vec![];
1✔
609

610
        for (parameter_name, parameter_metadata) in &self.parameter_names {
2✔
611
            let data_type = if let Some(data_type) = parameter_metadata.data_type.as_ref() {
1✔
612
                data_type.as_str().to_uppercase()
1✔
613
            } else {
614
                "FLOAT".to_string()
×
615
            };
616
            match data_type.as_str() {
1✔
617
                "STRING" => {
1✔
618
                    text.push(parameter_name.clone());
×
619
                }
×
620
                "INTEGER" => {
1✔
621
                    int.push(parameter_name.clone());
1✔
622
                }
1✔
623
                _ => {
×
624
                    float.push(parameter_name.clone());
×
625
                }
×
626
            }
627
        }
628
        OgrSourceColumnSpec {
1✔
629
            format_specifics: None,
1✔
630
            x: vector_spec.x,
1✔
631
            y: vector_spec.y,
1✔
632
            int,
1✔
633
            float,
1✔
634
            text,
1✔
635
            bool,
1✔
636
            datetime,
1✔
637
            rename: None,
1✔
638
        }
1✔
639
    }
1✔
640

641
    fn get_ogr_source_ds(
1✔
642
        &self,
1✔
643
        download_url: String,
1✔
644
        layer_name: String,
1✔
645
        vector_spec: EdrVectorSpec,
1✔
646
        cache_ttl: CacheTtlSeconds,
1✔
647
    ) -> OgrSourceDataset {
1✔
648
        OgrSourceDataset {
1✔
649
            file_name: download_url.into(),
1✔
650
            layer_name,
1✔
651
            data_type: Some(VectorDataType::MultiPoint),
1✔
652
            time: OgrSourceDatasetTimeType::Start {
1✔
653
                start_field: vector_spec.time.clone(),
1✔
654
                start_format: OgrSourceTimeFormat::Auto,
1✔
655
                duration: OgrSourceDurationSpec::Zero,
1✔
656
            },
1✔
657
            default_geometry: None,
1✔
658
            columns: Some(self.get_column_spec(vector_spec)),
1✔
659
            force_ogr_time_filter: false,
1✔
660
            force_ogr_spatial_filter: false,
1✔
661
            on_error: OgrSourceErrorSpec::Abort,
1✔
662
            sql_query: None,
1✔
663
            attribute_query: None,
1✔
664
            cache_ttl,
1✔
665
        }
1✔
666
    }
1✔
667

668
    fn get_ogr_metadata(
1✔
669
        &self,
1✔
670
        base_url: &Url,
1✔
671
        height: &str,
1✔
672
        vector_spec: EdrVectorSpec,
1✔
673
        cache_ttl: CacheTtlSeconds,
1✔
674
        discrete_vrs: &[String],
1✔
675
    ) -> Result<StaticMetaData<OgrSourceDataset, VectorResultDescriptor, VectorQueryRectangle>>
1✔
676
    {
1✔
677
        let (download_url, layer_name) =
1✔
678
            self.get_vector_download_url(base_url, height, discrete_vrs)?;
1✔
679
        let omd = self.get_ogr_source_ds(download_url, layer_name, vector_spec, cache_ttl);
1✔
680

1✔
681
        Ok(StaticMetaData {
1✔
682
            loading_info: omd,
1✔
683
            result_descriptor: self.get_vector_result_descriptor()?,
1✔
684
            phantom: Default::default(),
1✔
685
        })
686
    }
1✔
687

688
    fn get_raster_result_descriptor(
1✔
689
        &self,
1✔
690
    ) -> Result<RasterResultDescriptor, geoengine_operators::error::Error> {
1✔
691
        let bbox = self.get_bounding_box()?;
1✔
692
        let bbox = SpatialPartition2D::new_unchecked(bbox.upper_left(), bbox.lower_right());
1✔
693

1✔
694
        Ok(RasterResultDescriptor {
1✔
695
            data_type: RasterDataType::U8,
1✔
696
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
697
            time: Some(self.get_time_interval()?),
1✔
698
            bbox: Some(bbox),
1✔
699
            resolution: None,
1✔
700
            bands: RasterBandDescriptors::new_single_band(),
1✔
701
        })
702
    }
1✔
703

704
    fn get_gdal_loading_info_temporal_slice(
2✔
705
        &self,
2✔
706
        provider: &EdrDataProvider,
2✔
707
        parameter: &str,
2✔
708
        height: &str,
2✔
709
        data_time: TimeInterval,
2✔
710
        current_time: &str,
2✔
711
        dataset: &Dataset,
2✔
712
    ) -> Result<GdalLoadingInfoTemporalSlice, geoengine_operators::error::Error> {
2✔
713
        let rasterband = &dataset.rasterband(1)?;
2✔
714

715
        Ok(GdalLoadingInfoTemporalSlice {
716
            time: data_time,
2✔
717
            params: Some(GdalDatasetParameters {
2✔
718
                file_path: self
2✔
719
                    .get_raster_download_url(
2✔
720
                        &provider.base_url,
2✔
721
                        parameter,
2✔
722
                        height,
2✔
723
                        current_time,
2✔
724
                        &provider.discrete_vrs,
2✔
725
                    )?
2✔
726
                    .into(),
2✔
727
                rasterband_channel: 1,
2✔
728
                geo_transform: dataset
2✔
729
                    .geo_transform()
2✔
730
                    .context(crate::error::Gdal)
2✔
731
                    .map_err(|e| geoengine_operators::error::Error::LoadingInfo {
2✔
732
                        source: Box::new(e),
×
733
                    })?
2✔
734
                    .into(),
2✔
735
                width: rasterband.x_size(),
2✔
736
                height: rasterband.y_size(),
2✔
737
                file_not_found_handling: FileNotFoundHandling::NoData,
2✔
738
                no_data_value: None,
2✔
739
                properties_mapping: None,
2✔
740
                gdal_open_options: None,
2✔
741
                gdal_config_options: Some(vec![(
2✔
742
                    "GTIFF_HONOUR_NEGATIVE_SCALEY".to_string(),
2✔
743
                    "YES".to_string(),
2✔
744
                )]),
2✔
745
                allow_alphaband_as_mask: false,
2✔
746
                retry: None,
2✔
747
            }),
2✔
748
            cache_ttl: provider.cache_ttl,
2✔
749
        })
750
    }
2✔
751
}
752

753
#[derive(Deserialize)]
754
struct EdrExtents {
755
    spatial: Option<EdrSpatialExtent>,
756
    vertical: Option<EdrVerticalExtent>,
757
    temporal: Option<EdrTemporalExtent>,
758
}
759

760
impl EdrExtents {
761
    fn has_discrete_vertical_axis(&self, discrete_vrs: &[String]) -> bool {
3✔
762
        self.vertical
3✔
763
            .as_ref()
3✔
764
            .is_some_and(|val| discrete_vrs.contains(&val.vrs))
3✔
765
    }
3✔
766
}
767

768
#[derive(Deserialize)]
769
struct EdrSpatialExtent {
770
    bbox: Vec<Vec<f64>>,
771
}
772

773
#[derive(Deserialize)]
774
struct EdrVerticalExtent {
775
    values: Vec<String>,
776
    vrs: String,
777
}
778

779
#[derive(Deserialize, Clone)]
780
struct EdrTemporalExtent {
781
    interval: Vec<Vec<String>>,
782
    values: Vec<String>,
783
}
784

785
#[derive(Deserialize)]
786
struct EdrParameter {
787
    #[serde(rename = "data-type")]
788
    data_type: Option<String>,
789
    unit: Option<EdrUnit>,
790
    #[serde(rename = "observedProperty")]
791
    observed_property: ObservedProperty,
792
}
793

794
#[derive(Deserialize)]
795
struct EdrUnit {
796
    symbol: String,
797
}
798

799
#[derive(Deserialize)]
800
struct ObservedProperty {
801
    label: String,
802
}
803

804
enum EdrCollectionId {
805
    Collections,
806
    Collection {
807
        collection: String,
808
    },
809
    ParameterOrHeight {
810
        collection: String,
811
        parameter: String,
812
    },
813
    ParameterAndHeight {
814
        collection: String,
815
        parameter: String,
816
        height: String,
817
    },
818
}
819

820
impl EdrCollectionId {
821
    fn get_collection_id(&self) -> Result<&String> {
2✔
822
        match self {
2✔
823
            EdrCollectionId::Collections => Err(Error::InvalidLayerId),
×
824
            EdrCollectionId::Collection { collection }
1✔
825
            | EdrCollectionId::ParameterOrHeight { collection, .. }
×
826
            | EdrCollectionId::ParameterAndHeight { collection, .. } => Ok(collection),
2✔
827
        }
828
    }
2✔
829
}
830

831
impl FromStr for EdrCollectionId {
832
    type Err = Error;
833

834
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
10✔
835
        // Collection ids use ampersands as separators because some collection names
10✔
836
        // contain slashes.
10✔
837
        let split = s.split('!').collect::<Vec<_>>();
10✔
838

10✔
839
        Ok(match *split.as_slice() {
10✔
840
            ["collections"] => EdrCollectionId::Collections,
1✔
841
            ["collections", collection] => EdrCollectionId::Collection {
4✔
842
                collection: collection.to_string(),
4✔
843
            },
4✔
844
            ["collections", collection, parameter] => EdrCollectionId::ParameterOrHeight {
3✔
845
                collection: collection.to_string(),
3✔
846
                parameter: parameter.to_string(),
3✔
847
            },
3✔
848
            ["collections", collection, parameter, height] => EdrCollectionId::ParameterAndHeight {
2✔
849
                collection: collection.to_string(),
2✔
850
                parameter: parameter.to_string(),
2✔
851
                height: height.to_string(),
2✔
852
            },
2✔
853
            _ => return Err(Error::InvalidLayerCollectionId),
×
854
        })
855
    }
10✔
856
}
857

858
impl TryFrom<EdrCollectionId> for LayerCollectionId {
859
    type Error = Error;
860

861
    fn try_from(value: EdrCollectionId) -> std::result::Result<Self, Self::Error> {
5✔
862
        let s = match value {
5✔
863
            EdrCollectionId::Collections => "collections".to_string(),
×
864
            EdrCollectionId::Collection { collection } => format!("collections!{collection}"),
4✔
865
            EdrCollectionId::ParameterOrHeight {
866
                collection,
1✔
867
                parameter,
1✔
868
            } => format!("collections!{collection}!{parameter}"),
1✔
869
            EdrCollectionId::ParameterAndHeight { .. } => {
870
                return Err(Error::InvalidLayerCollectionId)
×
871
            }
872
        };
873

874
        Ok(LayerCollectionId(s))
5✔
875
    }
5✔
876
}
877

878
impl TryFrom<EdrCollectionId> for LayerId {
879
    type Error = Error;
880

881
    fn try_from(value: EdrCollectionId) -> std::result::Result<Self, Self::Error> {
5✔
882
        let s = match value {
5✔
883
            EdrCollectionId::Collections => return Err(Error::InvalidLayerId),
×
884
            EdrCollectionId::Collection { collection } => format!("collections!{collection}"),
1✔
885
            EdrCollectionId::ParameterOrHeight {
886
                collection,
2✔
887
                parameter,
2✔
888
            } => format!("collections!{collection}!{parameter}"),
2✔
889
            EdrCollectionId::ParameterAndHeight {
890
                collection,
2✔
891
                parameter,
2✔
892
                height,
2✔
893
            } => format!("collections!{collection}!{parameter}!{height}"),
2✔
894
        };
895

896
        Ok(LayerId(s))
5✔
897
    }
5✔
898
}
899

900
#[async_trait]
901
impl LayerCollectionProvider for EdrDataProvider {
902
    fn capabilities(&self) -> ProviderCapabilities {
×
903
        ProviderCapabilities {
×
904
            listing: true,
×
905
            search: SearchCapabilities::none(),
×
906
        }
×
907
    }
×
908

909
    fn name(&self) -> &str {
×
910
        &self.name
×
911
    }
×
912

913
    fn description(&self) -> &str {
×
914
        &self.description
×
915
    }
×
916

917
    async fn load_layer_collection(
918
        &self,
919
        collection_id: &LayerCollectionId,
920
        options: LayerCollectionListOptions,
921
    ) -> Result<LayerCollection> {
8✔
922
        let edr_id: EdrCollectionId = EdrCollectionId::from_str(&collection_id.0)
8✔
923
            .map_err(|_e| Error::InvalidLayerCollectionId)?;
8✔
924

925
        match edr_id {
8✔
926
            EdrCollectionId::Collections => self.get_root_collection(collection_id, &options).await,
1✔
927
            EdrCollectionId::Collection { collection } => {
3✔
928
                let collection_meta = self.load_collection_by_name(&collection).await?;
3✔
929

930
                if collection_meta.is_raster_file()? {
3✔
931
                    // The collection is of type raster. A layer can only contain one parameter
932
                    // of a raster dataset at a time, so let the user choose one.
933
                    self.get_raster_parameter_collection(collection_id, collection_meta, &options)
1✔
934
                } else if collection_meta.extent.vertical.is_some() {
2✔
935
                    // The collection is of type vector and data is provided for multiple heights.
936
                    // The user needs to be able to select the height he wants to see. It is not
937
                    // needed to select a parameter, because for vector datasets all parameters
938
                    // can be loaded simultaneously.
939
                    self.get_vector_height_collection(collection_id, collection_meta, &options)
1✔
940
                } else {
941
                    // The collection is of type vector and there is only data for a single height.
942
                    // No height or parameter needs to be selected by the user. Therefore the name
943
                    // of the collection already identifies a layer sufficiently.
944
                    Err(Error::InvalidLayerCollectionId)
1✔
945
                }
946
            }
947
            EdrCollectionId::ParameterOrHeight {
948
                collection,
3✔
949
                parameter,
3✔
950
            } => {
951
                let collection_meta = self.load_collection_by_name(&collection).await?;
3✔
952

953
                if !collection_meta.is_raster_file()? || collection_meta.extent.vertical.is_none() {
3✔
954
                    // When the collection is of type raster, the parameter-name is set by the
955
                    // parameter field. The height must not be selected when the collection has
956
                    // no height information.
957
                    // When the collection is of type vector, the height is already set by the
958
                    // parameter field. For vectors no parameter-name must be selected.
959
                    return Err(Error::InvalidLayerCollectionId);
2✔
960
                }
1✔
961
                // If the program gets here, it is a raster collection and it contains multiple
1✔
962
                // heights. The parameter-name was already chosen by the paramter field, but a
1✔
963
                // height must still be selected.
1✔
964
                self.get_raster_height_collection(
1✔
965
                    collection_id,
1✔
966
                    collection_meta,
1✔
967
                    &parameter,
1✔
968
                    &options,
1✔
969
                )
1✔
970
            }
971
            EdrCollectionId::ParameterAndHeight { .. } => Err(Error::InvalidLayerCollectionId),
1✔
972
        }
973
    }
16✔
974

975
    async fn get_root_layer_collection_id(&self) -> Result<LayerCollectionId> {
×
976
        EdrCollectionId::Collections.try_into()
×
977
    }
×
978

979
    async fn load_layer(&self, id: &LayerId) -> Result<Layer> {
×
980
        let edr_id: EdrCollectionId = EdrCollectionId::from_str(&id.0)?;
×
981
        let collection_id = edr_id.get_collection_id()?;
×
982

983
        let collection = self.load_collection_by_name(collection_id).await?;
×
984

985
        let operator = if collection.is_raster_file()? {
×
986
            TypedOperator::Raster(
×
987
                GdalSource {
×
988
                    params: GdalSourceParameters {
×
989
                        data: geoengine_datatypes::dataset::NamedData::with_system_provider(
×
990
                            self.id.to_string(),
×
991
                            id.to_string(),
×
992
                        ),
×
993
                    },
×
994
                }
×
995
                .boxed(),
×
996
            )
×
997
        } else {
998
            TypedOperator::Vector(
×
999
                OgrSource {
×
1000
                    params: OgrSourceParameters {
×
1001
                        data: geoengine_datatypes::dataset::NamedData::with_system_provider(
×
1002
                            self.id.to_string(),
×
1003
                            id.to_string(),
×
1004
                        ),
×
1005
                        attribute_projection: None,
×
1006
                        attribute_filters: None,
×
1007
                    },
×
1008
                }
×
1009
                .boxed(),
×
1010
            )
×
1011
        };
1012

1013
        Ok(Layer {
×
1014
            id: ProviderLayerId {
×
1015
                provider_id: self.id,
×
1016
                layer_id: id.clone(),
×
1017
            },
×
1018
            name: collection.title.unwrap_or(collection.id),
×
1019
            description: String::new(),
×
1020
            workflow: Workflow { operator },
×
1021
            symbology: None, // TODO
×
1022
            properties: vec![],
×
1023
            metadata: HashMap::new(),
×
1024
        })
×
1025
    }
×
1026
}
1027

1028
#[async_trait]
1029
impl
1030
    MetaDataProvider<MockDatasetDataSourceLoadingInfo, VectorResultDescriptor, VectorQueryRectangle>
1031
    for EdrDataProvider
1032
{
1033
    async fn meta_data(
1034
        &self,
1035
        _id: &geoengine_datatypes::dataset::DataId,
1036
    ) -> Result<
1037
        Box<
1038
            dyn MetaData<
1039
                MockDatasetDataSourceLoadingInfo,
1040
                VectorResultDescriptor,
1041
                VectorQueryRectangle,
1042
            >,
1043
        >,
1044
        geoengine_operators::error::Error,
1045
    > {
×
1046
        Err(geoengine_operators::error::Error::NotYetImplemented)
×
1047
    }
×
1048
}
1049

1050
#[async_trait]
1051
impl MetaDataProvider<OgrSourceDataset, VectorResultDescriptor, VectorQueryRectangle>
1052
    for EdrDataProvider
1053
{
1054
    async fn meta_data(
1055
        &self,
1056
        id: &geoengine_datatypes::dataset::DataId,
1057
    ) -> Result<
1058
        Box<dyn MetaData<OgrSourceDataset, VectorResultDescriptor, VectorQueryRectangle>>,
1059
        geoengine_operators::error::Error,
1060
    > {
1✔
1061
        let vector_spec = self.vector_spec.clone().ok_or_else(|| {
1✔
1062
            geoengine_operators::error::Error::DatasetMetaData {
×
1063
                source: Box::new(EdrProviderError::NoVectorSpecConfigured),
×
1064
            }
×
1065
        })?;
1✔
1066
        let (edr_id, collection) = self.load_collection_by_dataid(id).await?;
1✔
1067

1068
        let height = match edr_id {
1✔
1069
            EdrCollectionId::Collection { .. } => "default".to_string(),
1✔
1070
            EdrCollectionId::ParameterOrHeight { parameter, .. } => parameter,
×
1071
            _ => unreachable!(),
×
1072
        };
1073

1074
        let smd = collection
1✔
1075
            .get_ogr_metadata(
1✔
1076
                &self.base_url,
1✔
1077
                &height,
1✔
1078
                vector_spec,
1✔
1079
                self.cache_ttl,
1✔
1080
                &self.discrete_vrs,
1✔
1081
            )
1✔
1082
            .map_err(|e| geoengine_operators::error::Error::LoadingInfo {
1✔
1083
                source: Box::new(e),
×
1084
            })?;
1✔
1085

1086
        Ok(Box::new(smd))
1✔
1087
    }
2✔
1088
}
1089

1090
#[async_trait]
1091
impl MetaDataProvider<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>
1092
    for EdrDataProvider
1093
{
1094
    async fn meta_data(
1095
        &self,
1096
        id: &geoengine_datatypes::dataset::DataId,
1097
    ) -> Result<
1098
        Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>>,
1099
        geoengine_operators::error::Error,
1100
    > {
1✔
1101
        let (edr_id, collection) = self.load_collection_by_dataid(id).await?;
1✔
1102

1103
        let (parameter, height) = match edr_id {
1✔
1104
            EdrCollectionId::ParameterOrHeight { parameter, .. } => {
×
1105
                (parameter, "default".to_string())
×
1106
            }
1107
            EdrCollectionId::ParameterAndHeight {
1108
                parameter, height, ..
1✔
1109
            } => (parameter, height),
1✔
1110
            _ => unreachable!(),
×
1111
        };
1112

1113
        let mut params: Vec<GdalLoadingInfoTemporalSlice> = Vec::new();
1✔
1114
        // reverts the thread local configs on drop
1115
        let _thread_local_configs = TemporaryGdalThreadLocalConfigOptions::new(&[(
1✔
1116
            "GTIFF_HONOUR_NEGATIVE_SCALEY".to_string(),
1✔
1117
            "YES".to_string(),
1✔
1118
        )])?;
1✔
1119

1120
        if let Some(temporal_extent) = collection.extent.temporal.clone() {
1✔
1121
            let mut temporal_values_iter = temporal_extent.values.iter();
1✔
1122
            let mut previous_start = temporal_values_iter
1✔
1123
                .next()
1✔
1124
                // TODO: check if this could be unwrapped safely
1✔
1125
                .ok_or(
1✔
1126
                    geoengine_operators::error::Error::InvalidNumberOfTimeSteps {
1✔
1127
                        expected: 1,
1✔
1128
                        found: 0,
1✔
1129
                    },
1✔
1130
                )?;
1✔
1131
            let dataset = gdal_open_dataset(
1✔
1132
                collection
1✔
1133
                    .get_raster_download_url(
1✔
1134
                        &self.base_url,
1✔
1135
                        &parameter,
1✔
1136
                        &height,
1✔
1137
                        previous_start,
1✔
1138
                        &self.discrete_vrs,
1✔
1139
                    )?
1✔
1140
                    .as_ref(),
1✔
1141
            )?;
×
1142

1143
            for current_time in temporal_values_iter {
2✔
1144
                params.push(collection.get_gdal_loading_info_temporal_slice(
1✔
1145
                    self,
1✔
1146
                    &parameter,
1✔
1147
                    &height,
1✔
1148
                    time_interval_from_strings(previous_start, current_time)?,
1✔
1149
                    previous_start,
1✔
1150
                    &dataset,
1✔
1151
                )?);
×
1152
                previous_start = current_time;
1✔
1153
            }
1154
            params.push(collection.get_gdal_loading_info_temporal_slice(
1✔
1155
                self,
1✔
1156
                &parameter,
1✔
1157
                &height,
1✔
1158
                time_interval_from_strings(previous_start, &temporal_extent.interval[0][1])?,
1✔
1159
                previous_start,
1✔
1160
                &dataset,
1✔
1161
            )?);
×
1162
        } else {
1163
            let dummy_time = "2023-06-06T00:00:00Z";
×
1164
            let dataset = gdal_open_dataset(
×
1165
                collection
×
1166
                    .get_raster_download_url(
×
1167
                        &self.base_url,
×
1168
                        &parameter,
×
1169
                        &height,
×
1170
                        dummy_time,
×
1171
                        &self.discrete_vrs,
×
1172
                    )?
×
1173
                    .as_ref(),
×
1174
            )?;
×
1175
            params.push(collection.get_gdal_loading_info_temporal_slice(
×
1176
                self,
×
1177
                &parameter,
×
1178
                &height,
×
1179
                TimeInterval::default(),
×
1180
                dummy_time,
×
1181
                &dataset,
×
1182
            )?);
×
1183
        }
1184

1185
        Ok(Box::new(GdalMetaDataList {
1186
            result_descriptor: collection.get_raster_result_descriptor()?,
1✔
1187
            params,
1✔
1188
        }))
1189
    }
2✔
1190
}
1191

1192
// TODO: proper error handling
1193
#[allow(clippy::unnecessary_wraps)]
1194
fn time_interval_from_strings(
4✔
1195
    start: &str,
4✔
1196
    end: &str,
4✔
1197
) -> Result<TimeInterval, geoengine_operators::error::Error> {
4✔
1198
    let start = TimeInstance::from_str(start).unwrap_or(TimeInstance::MIN);
4✔
1199
    let end = TimeInstance::from_str(end).unwrap_or(TimeInstance::MAX);
4✔
1200
    Ok(TimeInterval::new_unchecked(start, end))
4✔
1201
}
4✔
1202

1203
#[derive(Debug, Snafu)]
×
1204
#[snafu(visibility(pub(crate)))]
1205
#[snafu(context(suffix(false)))] // disables default `Snafu` suffix
1206
pub enum EdrProviderError {
1207
    MissingSpatialExtent,
1208
    MissingTemporalExtent,
1209
    NoSupportedOutputFormat,
1210
    NoVectorSpecConfigured,
1211
}
1212

1213
#[cfg(test)]
1214
mod tests {
1215
    use super::*;
1216
    use crate::{
1217
        contexts::SessionContext,
1218
        contexts::{PostgresDb, PostgresSessionContext},
1219
        ge_context,
1220
    };
1221
    use geoengine_datatypes::{
1222
        dataset::ExternalDataId,
1223
        primitives::{BandSelection, ColumnSelection, SpatialResolution},
1224
        util::gdal::hide_gdal_errors,
1225
    };
1226
    use geoengine_operators::{engine::ResultDescriptor, source::GdalDatasetGeoTransform};
1227
    use httptest::{matchers::*, responders::status_code, Expectation, Server};
1228
    use std::{ops::Range, path::PathBuf};
1229
    use tokio_postgres::NoTls;
1230

1231
    const DEMO_PROVIDER_ID: DataProviderId =
1232
        DataProviderId::from_u128(0xdc2d_dc34_b0d9_4ee0_bf3e_414f_01a8_05ad);
1233

1234
    fn test_data_path(file_name: &str) -> PathBuf {
11✔
1235
        crate::test_data!(String::from("edr/") + file_name).into()
11✔
1236
    }
11✔
1237

1238
    async fn create_provider<D: GeoEngineDb>(server: &Server, db: D) -> Box<dyn DataProvider> {
10✔
1239
        Box::new(EdrDataProviderDefinition {
10✔
1240
            name: "EDR".to_string(),
10✔
1241
            description: "Environmental Data Retrieval".to_string(),
10✔
1242
            priority: None,
10✔
1243
            id: DEMO_PROVIDER_ID,
10✔
1244
            base_url: Url::parse(server.url_str("").strip_suffix('/').unwrap()).unwrap(),
10✔
1245
            vector_spec: Some(EdrVectorSpec {
10✔
1246
                x: "geometry".to_string(),
10✔
1247
                y: None,
10✔
1248
                time: "time".to_string(),
10✔
1249
            }),
10✔
1250
            cache_ttl: Default::default(),
10✔
1251
            discrete_vrs: vec!["between-depth".to_string()],
10✔
1252
            provenance: None,
10✔
1253
        })
10✔
1254
        .initialize(db)
10✔
1255
        .await
10✔
1256
        .unwrap()
10✔
1257
    }
10✔
1258

1259
    async fn setup_url(
11✔
1260
        server: &mut Server,
11✔
1261
        url: &str,
11✔
1262
        content_type: &str,
11✔
1263
        file_name: &str,
11✔
1264
        times: Range<usize>,
11✔
1265
    ) {
11✔
1266
        let path = test_data_path(file_name);
11✔
1267
        let body = tokio::fs::read(path).await.unwrap();
11✔
1268

11✔
1269
        let responder = status_code(200)
11✔
1270
            .append_header("content-type", content_type.to_owned())
11✔
1271
            .append_header("content-length", body.len())
11✔
1272
            .body(body);
11✔
1273

11✔
1274
        server.expect(
11✔
1275
            Expectation::matching(request::method_path("GET", url.to_string()))
11✔
1276
                .times(times)
11✔
1277
                .respond_with(responder),
11✔
1278
        );
11✔
1279
    }
11✔
1280

1281
    async fn load_layer_collection<D: GeoEngineDb>(
8✔
1282
        collection: &LayerCollectionId,
8✔
1283
        db: D,
8✔
1284
    ) -> Result<LayerCollection> {
8✔
1285
        let mut server = Server::run();
8✔
1286

8✔
1287
        if collection.0 == "collections" {
8✔
1288
            setup_url(
1✔
1289
                &mut server,
1✔
1290
                "/collections",
1✔
1291
                "application/json",
1✔
1292
                "edr_collections.json",
1✔
1293
                0..2,
1✔
1294
            )
1✔
1295
            .await;
1✔
1296
        } else {
1297
            let collection_name = collection.0.split('!').nth(1).unwrap();
7✔
1298
            setup_url(
7✔
1299
                &mut server,
7✔
1300
                &format!("/collections/{collection_name}"),
7✔
1301
                "application/json",
7✔
1302
                &format!("edr_{collection_name}.json"),
7✔
1303
                0..2,
7✔
1304
            )
7✔
1305
            .await;
7✔
1306
        }
1307

1308
        let provider = create_provider(&server, db).await;
8✔
1309

1310
        let datasets = provider
8✔
1311
            .load_layer_collection(
8✔
1312
                collection,
8✔
1313
                LayerCollectionListOptions {
8✔
1314
                    offset: 0,
8✔
1315
                    limit: 20,
8✔
1316
                },
8✔
1317
            )
8✔
1318
            .await?;
8✔
1319
        server.verify_and_clear();
4✔
1320

4✔
1321
        Ok(datasets)
4✔
1322
    }
8✔
1323

1324
    #[ge_context::test]
1✔
1325
    async fn it_loads_root_collection(ctx: PostgresSessionContext<NoTls>) -> Result<()> {
1✔
1326
        let root_collection_id = LayerCollectionId("collections".to_string());
1✔
1327
        let datasets = load_layer_collection(&root_collection_id, ctx.db()).await?;
1✔
1328

1329
        assert_eq!(
1✔
1330
            datasets,
1✔
1331
            LayerCollection {
1✔
1332
                id: ProviderLayerCollectionId {
1✔
1333
                    provider_id: DEMO_PROVIDER_ID,
1✔
1334
                    collection_id: root_collection_id
1✔
1335
                },
1✔
1336
                name: "EDR".to_owned(),
1✔
1337
                description: "Environmental Data Retrieval".to_owned(),
1✔
1338
                items: vec![
1✔
1339
                    // Note: The dataset GFS_single-level_50 gets filtered out because there is no extent set.
1✔
1340
                    // This means that it contains no data.
1✔
1341
                    CollectionItem::Collection(LayerCollectionListing {
1✔
1342
                        r#type: Default::default(),
1✔
1343
                        id: ProviderLayerCollectionId {
1✔
1344
                            provider_id: DEMO_PROVIDER_ID,
1✔
1345
                            collection_id: LayerCollectionId(
1✔
1346
                                "collections!GFS_single-level".to_string()
1✔
1347
                            )
1✔
1348
                        },
1✔
1349
                        name: "GFS - Single Level".to_string(),
1✔
1350
                        description: String::new(),
1✔
1351
                        properties: vec![],
1✔
1352
                    }),
1✔
1353
                    CollectionItem::Collection(LayerCollectionListing {
1✔
1354
                        r#type: Default::default(),
1✔
1355
                        id: ProviderLayerCollectionId {
1✔
1356
                            provider_id: DEMO_PROVIDER_ID,
1✔
1357
                            collection_id: LayerCollectionId(
1✔
1358
                                "collections!GFS_isobaric".to_string()
1✔
1359
                            )
1✔
1360
                        },
1✔
1361
                        name: "GFS - Isobaric level".to_string(),
1✔
1362
                        description: String::new(),
1✔
1363
                        properties: vec![],
1✔
1364
                    }),
1✔
1365
                    CollectionItem::Collection(LayerCollectionListing {
1✔
1366
                        r#type: Default::default(),
1✔
1367
                        id: ProviderLayerCollectionId {
1✔
1368
                            provider_id: DEMO_PROVIDER_ID,
1✔
1369
                            collection_id: LayerCollectionId(
1✔
1370
                                "collections!GFS_between-depth".to_string()
1✔
1371
                            )
1✔
1372
                        },
1✔
1373
                        name: "GFS - Layer between two depths below land surface".to_string(),
1✔
1374
                        description: String::new(),
1✔
1375
                        properties: vec![],
1✔
1376
                    }),
1✔
1377
                    CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
1378
                        id: ProviderLayerId {
1✔
1379
                            provider_id: DEMO_PROVIDER_ID,
1✔
1380
                            layer_id: LayerId("collections!PointsInGermany".to_string())
1✔
1381
                        },
1✔
1382
                        name: "PointsInGermany".to_string(),
1✔
1383
                        description: String::new(),
1✔
1384
                        properties: vec![],
1✔
1385
                    }),
1✔
1386
                    CollectionItem::Collection(LayerCollectionListing {
1✔
1387
                        r#type: Default::default(),
1✔
1388
                        id: ProviderLayerCollectionId {
1✔
1389
                            provider_id: DEMO_PROVIDER_ID,
1✔
1390
                            collection_id: LayerCollectionId(
1✔
1391
                                "collections!PointsInFrance".to_string()
1✔
1392
                            )
1✔
1393
                        },
1✔
1394
                        name: "PointsInFrance".to_string(),
1✔
1395
                        description: String::new(),
1✔
1396
                        properties: vec![],
1✔
1397
                    }),
1✔
1398
                ],
1✔
1399
                entry_label: None,
1✔
1400
                properties: vec![]
1✔
1401
            }
1✔
1402
        );
1✔
1403

1404
        Ok(())
1✔
1405
    }
1✔
1406

1407
    #[ge_context::test]
1✔
1408
    async fn it_loads_raster_parameter_collection(
1✔
1409
        ctx: PostgresSessionContext<NoTls>,
1✔
1410
    ) -> Result<()> {
1✔
1411
        let collection_id = LayerCollectionId("collections!GFS_isobaric".to_string());
1✔
1412
        let datasets = load_layer_collection(&collection_id, ctx.db()).await?;
1✔
1413

1414
        assert_eq!(
1✔
1415
            datasets,
1✔
1416
            LayerCollection {
1✔
1417
                id: ProviderLayerCollectionId {
1✔
1418
                    provider_id: DEMO_PROVIDER_ID,
1✔
1419
                    collection_id
1✔
1420
                },
1✔
1421
                name: "GFS_isobaric".to_owned(),
1✔
1422
                description: "Parameters of GFS_isobaric".to_owned(),
1✔
1423
                items: vec![CollectionItem::Collection(LayerCollectionListing {
1✔
1424
                    r#type: Default::default(),
1✔
1425
                    id: ProviderLayerCollectionId {
1✔
1426
                        provider_id: DEMO_PROVIDER_ID,
1✔
1427
                        collection_id: LayerCollectionId(
1✔
1428
                            "collections!GFS_isobaric!temperature".to_string()
1✔
1429
                        )
1✔
1430
                    },
1✔
1431
                    name: "temperature".to_string(),
1✔
1432
                    description: String::new(),
1✔
1433
                    properties: vec![],
1✔
1434
                })],
1✔
1435
                entry_label: None,
1✔
1436
                properties: vec![]
1✔
1437
            }
1✔
1438
        );
1✔
1439

1440
        Ok(())
1✔
1441
    }
1✔
1442

1443
    #[ge_context::test]
1✔
1444
    async fn it_loads_vector_height_collection(ctx: PostgresSessionContext<NoTls>) -> Result<()> {
1✔
1445
        let collection_id = LayerCollectionId("collections!PointsInFrance".to_string());
1✔
1446
        let datasets = load_layer_collection(&collection_id, ctx.db()).await?;
1✔
1447

1448
        assert_eq!(
1✔
1449
            datasets,
1✔
1450
            LayerCollection {
1✔
1451
                id: ProviderLayerCollectionId {
1✔
1452
                    provider_id: DEMO_PROVIDER_ID,
1✔
1453
                    collection_id
1✔
1454
                },
1✔
1455
                name: "PointsInFrance".to_owned(),
1✔
1456
                description: "Height selection of PointsInFrance".to_owned(),
1✔
1457
                items: vec![
1✔
1458
                    CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
1459
                        id: ProviderLayerId {
1✔
1460
                            provider_id: DEMO_PROVIDER_ID,
1✔
1461
                            layer_id: LayerId("collections!PointsInFrance!0\\10cm".to_string())
1✔
1462
                        },
1✔
1463
                        name: "0\\10cm".to_string(),
1✔
1464
                        description: String::new(),
1✔
1465
                        properties: vec![],
1✔
1466
                    }),
1✔
1467
                    CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
1468
                        id: ProviderLayerId {
1✔
1469
                            provider_id: DEMO_PROVIDER_ID,
1✔
1470
                            layer_id: LayerId("collections!PointsInFrance!10\\40cm".to_string())
1✔
1471
                        },
1✔
1472
                        name: "10\\40cm".to_string(),
1✔
1473
                        description: String::new(),
1✔
1474
                        properties: vec![],
1✔
1475
                    })
1✔
1476
                ],
1✔
1477
                entry_label: None,
1✔
1478
                properties: vec![]
1✔
1479
            }
1✔
1480
        );
1✔
1481

1482
        Ok(())
1✔
1483
    }
1✔
1484

1485
    #[ge_context::test]
1✔
1486
    async fn vector_without_height_collection_invalid(ctx: PostgresSessionContext<NoTls>) {
1✔
1487
        let collection_id = LayerCollectionId("collections!PointsInGermany".to_string());
1✔
1488
        let res = load_layer_collection(&collection_id, ctx.db()).await;
1✔
1489

1490
        assert!(res.is_err());
1✔
1491
    }
1✔
1492

1493
    #[ge_context::test]
1✔
1494
    async fn it_loads_raster_height_collection(ctx: PostgresSessionContext<NoTls>) -> Result<()> {
1✔
1495
        let collection_id = LayerCollectionId("collections!GFS_isobaric!temperature".to_string());
1✔
1496
        let datasets = load_layer_collection(&collection_id, ctx.db()).await?;
1✔
1497

1498
        assert_eq!(
1✔
1499
            datasets,
1✔
1500
            LayerCollection {
1✔
1501
                id: ProviderLayerCollectionId {
1✔
1502
                    provider_id: DEMO_PROVIDER_ID,
1✔
1503
                    collection_id
1✔
1504
                },
1✔
1505
                name: "GFS_isobaric".to_owned(),
1✔
1506
                description: "Height selection of GFS_isobaric".to_owned(),
1✔
1507
                items: vec![
1✔
1508
                    CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
1509
                        id: ProviderLayerId {
1✔
1510
                            provider_id: DEMO_PROVIDER_ID,
1✔
1511
                            layer_id: LayerId(
1✔
1512
                                "collections!GFS_isobaric!temperature!0.01".to_string()
1✔
1513
                            )
1✔
1514
                        },
1✔
1515
                        name: "0.01".to_string(),
1✔
1516
                        description: String::new(),
1✔
1517
                        properties: vec![],
1✔
1518
                    }),
1✔
1519
                    CollectionItem::Layer(LayerListing { r#type: Default::default(),
1✔
1520
                        id: ProviderLayerId {
1✔
1521
                            provider_id: DEMO_PROVIDER_ID,
1✔
1522
                            layer_id: LayerId(
1✔
1523
                                "collections!GFS_isobaric!temperature!1000".to_string()
1✔
1524
                            )
1✔
1525
                        },
1✔
1526
                        name: "1000".to_string(),
1✔
1527
                        description: String::new(),
1✔
1528
                        properties: vec![],
1✔
1529
                    })
1✔
1530
                ],
1✔
1531
                entry_label: None,
1✔
1532
                properties: vec![]
1✔
1533
            }
1✔
1534
        );
1✔
1535

1536
        Ok(())
1✔
1537
    }
1✔
1538

1539
    #[ge_context::test]
1✔
1540
    async fn vector_with_parameter_collection_invalid(
1✔
1541
        ctx: PostgresSessionContext<NoTls>,
1✔
1542
    ) -> Result<()> {
1✔
1543
        let collection_id = LayerCollectionId("collections!PointsInGermany!ID".to_string());
1✔
1544
        let res = load_layer_collection(&collection_id, ctx.db()).await;
1✔
1545

1546
        assert!(res.is_err());
1✔
1547

1548
        Ok(())
1✔
1549
    }
1✔
1550

1551
    #[ge_context::test]
1✔
1552
    async fn raster_with_parameter_without_height_collection_invalid(
1✔
1553
        ctx: PostgresSessionContext<NoTls>,
1✔
1554
    ) -> Result<()> {
1✔
1555
        let collection_id =
1✔
1556
            LayerCollectionId("collections!GFS_single-level!temperature_max-wind".to_string());
1✔
1557
        let res = load_layer_collection(&collection_id, ctx.db()).await;
1✔
1558

1559
        assert!(res.is_err());
1✔
1560

1561
        Ok(())
1✔
1562
    }
1✔
1563

1564
    #[ge_context::test]
1✔
1565
    async fn collection_with_parameter_and_height_invalid(
1✔
1566
        ctx: PostgresSessionContext<NoTls>,
1✔
1567
    ) -> Result<()> {
1✔
1568
        let collection_id =
1✔
1569
            LayerCollectionId("collections!GFS_isobaric!temperature!1000".to_string());
1✔
1570
        let res = load_layer_collection(&collection_id, ctx.db()).await;
1✔
1571

1572
        assert!(res.is_err());
1✔
1573

1574
        Ok(())
1✔
1575
    }
1✔
1576

1577
    async fn load_metadata<L, R, Q, D: GeoEngineDb>(
2✔
1578
        server: &mut Server,
2✔
1579
        collection: &'static str,
2✔
1580
        db: D,
2✔
1581
    ) -> Box<dyn MetaData<L, R, Q>>
2✔
1582
    where
2✔
1583
        R: ResultDescriptor,
2✔
1584
        dyn DataProvider: MetaDataProvider<L, R, Q>,
2✔
1585
    {
2✔
1586
        let collection_name = collection.split('!').next().unwrap();
2✔
1587
        setup_url(
2✔
1588
            server,
2✔
1589
            &format!("/collections/{collection_name}"),
2✔
1590
            "application/json",
2✔
1591
            &format!("edr_{collection_name}.json"),
2✔
1592
            1..2,
2✔
1593
        )
2✔
1594
        .await;
2✔
1595

1596
        let provider = create_provider(server, db).await;
2✔
1597

1598
        let meta: Box<dyn MetaData<L, R, Q>> = provider
2✔
1599
            .meta_data(&DataId::External(ExternalDataId {
2✔
1600
                provider_id: DEMO_PROVIDER_ID,
2✔
1601
                layer_id: LayerId(format!("collections!{collection}")),
2✔
1602
            }))
2✔
1603
            .await
2✔
1604
            .unwrap();
2✔
1605
        server.verify_and_clear();
2✔
1606
        meta
2✔
1607
    }
2✔
1608

1609
    #[ge_context::test]
1✔
1610
    async fn generate_ogr_metadata(ctx: PostgresSessionContext<NoTls>) {
1✔
1611
        let mut server = Server::run();
1✔
1612
        let meta = load_metadata::<
1✔
1613
            OgrSourceDataset,
1✔
1614
            VectorResultDescriptor,
1✔
1615
            VectorQueryRectangle,
1✔
1616
            PostgresDb<NoTls>,
1✔
1617
        >(&mut server, "PointsInGermany", ctx.db())
1✔
1618
        .await;
1✔
1619
        let loading_info = meta
1✔
1620
            .loading_info(VectorQueryRectangle {
1✔
1621
                spatial_bounds: BoundingBox2D::new_unchecked(
1✔
1622
                    (-180., -90.).into(),
1✔
1623
                    (180., 90.).into(),
1✔
1624
                ),
1✔
1625
                time_interval: TimeInterval::default(),
1✔
1626
                spatial_resolution: SpatialResolution::zero_point_one(),
1✔
1627
                attributes: ColumnSelection::all(),
1✔
1628
            })
1✔
1629
            .await
1✔
1630
            .unwrap();
1✔
1631
        assert_eq!(
1✔
1632
            loading_info,
1✔
1633
            OgrSourceDataset {
1✔
1634
                file_name: format!("/vsicurl_streaming/{}", server.url_str("/collections/PointsInGermany/cube?bbox=-180,-90,180,90&datetime=2023-01-01T12:42:29Z%2F2023-02-01T12:42:29Z&f=GeoJSON")).into(),
1✔
1635
                layer_name: "cube?bbox=-180,-90,180,90&datetime=2023-01-01T12:42:29Z%2F2023-02-01T12:42:29Z&f=GeoJSON".to_string(),
1✔
1636
                data_type: Some(VectorDataType::MultiPoint),
1✔
1637
                time: OgrSourceDatasetTimeType::Start {
1✔
1638
                    start_field: "time".to_string(),
1✔
1639
                    start_format: OgrSourceTimeFormat::Auto,
1✔
1640
                    duration: OgrSourceDurationSpec::Zero,
1✔
1641
                },
1✔
1642
                default_geometry: None,
1✔
1643
                columns: Some(OgrSourceColumnSpec {
1✔
1644
                    format_specifics: None,
1✔
1645
                    x: "geometry".to_string(),
1✔
1646
                    y: None,
1✔
1647
                    int: vec!["ID".to_string()],
1✔
1648
                    float: vec![],
1✔
1649
                    text: vec![],
1✔
1650
                    bool: vec![],
1✔
1651
                    datetime: vec![],
1✔
1652
                    rename: None,
1✔
1653
                }),
1✔
1654
                force_ogr_time_filter: false,
1✔
1655
                force_ogr_spatial_filter: false,
1✔
1656
                on_error: OgrSourceErrorSpec::Abort,
1✔
1657
                sql_query: None,
1✔
1658
                attribute_query: None,
1✔
1659
                cache_ttl: Default::default(),
1✔
1660
            }
1✔
1661
        );
1✔
1662

1663
        let result_descriptor = meta.result_descriptor().await.unwrap();
1✔
1664
        assert_eq!(
1✔
1665
            result_descriptor,
1✔
1666
            VectorResultDescriptor {
1✔
1667
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1668
                data_type: VectorDataType::MultiPoint,
1✔
1669
                columns: hashmap! {
1✔
1670
                    "ID".to_string() => VectorColumnInfo {
1✔
1671
                        data_type: FeatureDataType::Int,
1✔
1672
                        measurement: Measurement::Continuous(ContinuousMeasurement {
1✔
1673
                            measurement: "ID".to_string(),
1✔
1674
                            unit: None,
1✔
1675
                        }),
1✔
1676
                    }
1✔
1677
                },
1✔
1678
                time: Some(TimeInterval::new_unchecked(
1✔
1679
                    1_672_576_949_000,
1✔
1680
                    1_675_255_349_000,
1✔
1681
                )),
1✔
1682
                bbox: Some(BoundingBox2D::new_unchecked(
1✔
1683
                    (-180., -90.).into(),
1✔
1684
                    (180., 90.).into()
1✔
1685
                )),
1✔
1686
            }
1✔
1687
        );
1✔
1688
    }
1✔
1689

1690
    #[ge_context::test]
1✔
1691
    #[allow(clippy::too_many_lines)]
1692
    async fn generate_gdal_metadata(ctx: PostgresSessionContext<NoTls>) {
1✔
1693
        hide_gdal_errors(); //hide GTIFF_HONOUR_NEGATIVE_SCALEY warning
1✔
1694

1✔
1695
        let mut server = Server::run();
1✔
1696
        setup_url(
1✔
1697
            &mut server,
1✔
1698
            "/collections/GFS_isobaric/cube",
1✔
1699
            "image/tiff",
1✔
1700
            "edr_raster.tif",
1✔
1701
            3..5,
1✔
1702
        )
1✔
1703
        .await;
1✔
1704
        server.expect(
1✔
1705
            Expectation::matching(all_of![
1✔
1706
                request::method_path("HEAD", "/collections/GFS_isobaric/cube"),
1✔
1707
                request::query(url_decoded(contains((
1✔
1708
                    "parameter-name",
1✔
1709
                    "temperature.aux.xml"
1✔
1710
                ))))
1✔
1711
            ])
1✔
1712
            .times(0..2)
1✔
1713
            .respond_with(status_code(404)),
1✔
1714
        );
1✔
1715
        let meta = load_metadata::<
1✔
1716
            GdalLoadingInfo,
1✔
1717
            RasterResultDescriptor,
1✔
1718
            RasterQueryRectangle,
1✔
1719
            PostgresDb<NoTls>,
1✔
1720
        >(&mut server, "GFS_isobaric!temperature!1000", ctx.db())
1✔
1721
        .await;
1✔
1722

1723
        let loading_info_parts = meta
1✔
1724
            .loading_info(RasterQueryRectangle {
1✔
1725
                spatial_bounds: SpatialPartition2D::new_unchecked(
1✔
1726
                    (0., 90.).into(),
1✔
1727
                    (360., -90.).into(),
1✔
1728
                ),
1✔
1729
                time_interval: TimeInterval::new_unchecked(1_692_144_000_000, 1_692_500_400_000),
1✔
1730
                spatial_resolution: SpatialResolution::new_unchecked(1., 1.),
1✔
1731
                attributes: BandSelection::first(),
1✔
1732
            })
1✔
1733
            .await
1✔
1734
            .unwrap()
1✔
1735
            .info
1✔
1736
            .map(Result::unwrap)
1✔
1737
            .collect::<Vec<_>>();
1✔
1738
        assert_eq!(
1✔
1739
            loading_info_parts,
1✔
1740
            vec![
1✔
1741
                GdalLoadingInfoTemporalSlice {
1✔
1742
                    time: TimeInterval::new_unchecked(
1✔
1743
                        1_692_144_000_000, 1_692_154_800_000
1✔
1744
                    ),
1✔
1745
                    params: Some(GdalDatasetParameters {
1✔
1746
                        file_path: format!("/vsicurl_streaming/{}", server.url_str("/collections/GFS_isobaric/cube?bbox=0,-90,359.50000000000006,90&z=1000%2F1000&datetime=2023-08-16T00:00:00Z%2F2023-08-16T00:00:00Z&f=GeoTIFF&parameter-name=temperature")).into(),
1✔
1747
                        rasterband_channel: 1,
1✔
1748
                        geo_transform: GdalDatasetGeoTransform {
1✔
1749
                            origin_coordinate: (0., -90.).into(),
1✔
1750
                            x_pixel_size: 0.499_305_555_555_555_6,
1✔
1751
                            y_pixel_size: 0.498_614_958_448_753_5,
1✔
1752
                        },
1✔
1753
                        width: 720,
1✔
1754
                        height: 361,
1✔
1755
                        file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1756
                        no_data_value: None,
1✔
1757
                        properties_mapping: None,
1✔
1758
                        gdal_open_options: None,
1✔
1759
                        gdal_config_options: Some(vec![(
1✔
1760
                            "GTIFF_HONOUR_NEGATIVE_SCALEY".to_string(),
1✔
1761
                            "YES".to_string(),
1✔
1762
                        )]),
1✔
1763
                        allow_alphaband_as_mask: false,
1✔
1764
                        retry: None,
1✔
1765
                    }),
1✔
1766
                    cache_ttl: Default::default(),
1✔
1767
                },
1✔
1768
                GdalLoadingInfoTemporalSlice {
1✔
1769
                    time: TimeInterval::new_unchecked(
1✔
1770
                        1_692_154_800_000, 1_692_500_400_000
1✔
1771
                    ),
1✔
1772
                    params: Some(GdalDatasetParameters {
1✔
1773
                        file_path: format!("/vsicurl_streaming/{}", server.url_str("/collections/GFS_isobaric/cube?bbox=0,-90,359.50000000000006,90&z=1000%2F1000&datetime=2023-08-16T03:00:00Z%2F2023-08-16T03:00:00Z&f=GeoTIFF&parameter-name=temperature")).into(),
1✔
1774
                        rasterband_channel: 1,
1✔
1775
                        geo_transform: GdalDatasetGeoTransform {
1✔
1776
                            origin_coordinate: (0., -90.).into(),
1✔
1777
                            x_pixel_size: 0.499_305_555_555_555_6,
1✔
1778
                            y_pixel_size: 0.498_614_958_448_753_5,
1✔
1779
                        },
1✔
1780
                        width: 720,
1✔
1781
                        height: 361,
1✔
1782
                        file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1783
                        no_data_value: None,
1✔
1784
                        properties_mapping: None,
1✔
1785
                        gdal_open_options: None,
1✔
1786
                        gdal_config_options: Some(vec![(
1✔
1787
                            "GTIFF_HONOUR_NEGATIVE_SCALEY".to_string(),
1✔
1788
                            "YES".to_string(),
1✔
1789
                        )]),
1✔
1790
                        allow_alphaband_as_mask: false,
1✔
1791
                        retry: None,
1✔
1792
                    }),
1✔
1793
                    cache_ttl: Default::default(),
1✔
1794
                }
1✔
1795
            ]
1✔
1796
        );
1✔
1797

1798
        let result_descriptor = meta.result_descriptor().await.unwrap();
1✔
1799
        assert_eq!(
1✔
1800
            result_descriptor,
1✔
1801
            RasterResultDescriptor {
1✔
1802
                data_type: RasterDataType::U8,
1✔
1803
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1804
                time: Some(TimeInterval::new_unchecked(
1✔
1805
                    1_692_144_000_000,
1✔
1806
                    1_692_500_400_000
1✔
1807
                )),
1✔
1808
                bbox: Some(SpatialPartition2D::new_unchecked(
1✔
1809
                    (0., 90.).into(),
1✔
1810
                    (359.500_000_000_000_06, -90.).into()
1✔
1811
                )),
1✔
1812
                resolution: None,
1✔
1813
                bands: RasterBandDescriptors::new_single_band(),
1✔
1814
            }
1✔
1815
        );
1✔
1816
    }
1✔
1817
}
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