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

geo-engine / geoengine / 4136700900

pending completion
4136700900

push

github

GitHub
Merge #736

2 of 2 new or added lines in 1 file covered. (100.0%)

90238 of 102526 relevant lines covered (88.01%)

75266.83 hits per line

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

93.59
/operators/src/source/gdal_source/mod.rs
1
use crate::adapters::SparseTilesFillAdapter;
2
use crate::engine::{CreateSpan, MetaData, OperatorData, OperatorName, QueryProcessor};
3
use crate::util::gdal::gdal_open_dataset_ex;
4
use crate::util::input::float_option_with_nan;
5
use crate::util::retry::retry;
6
use crate::util::TemporaryGdalThreadLocalConfigOptions;
7
use crate::{
8
    engine::{
9
        InitializedRasterOperator, RasterOperator, RasterQueryProcessor, RasterResultDescriptor,
10
        SourceOperator, TypedRasterQueryProcessor,
11
    },
12
    error::Error,
13
    util::Result,
14
};
15
use async_trait::async_trait;
16
pub use error::GdalSourceError;
17
use float_cmp::{approx_eq, ApproxEq};
18
use futures::{
19
    stream::{self, BoxStream, StreamExt},
20
    Stream,
21
};
22
use futures::{Future, TryStreamExt};
23
use gdal::errors::GdalError;
24
use gdal::raster::{GdalType, RasterBand as GdalRasterBand};
25
use gdal::{Dataset as GdalDataset, DatasetOptions, GdalOpenFlags, Metadata as GdalMetadata};
26
use gdal_sys::VSICurlPartialClearCache;
27
use geoengine_datatypes::primitives::{
28
    AxisAlignedRectangle, Coordinate2D, DateTimeParseFormat, RasterQueryRectangle,
29
    SpatialPartition2D, SpatialPartitioned,
30
};
31
use geoengine_datatypes::raster::{
32
    EmptyGrid, GeoTransform, GridIdx2D, GridOrEmpty, GridOrEmpty2D, GridShape2D, GridShapeAccess,
33
    MapElements, MaskedGrid, NoDataValueGrid, Pixel, RasterDataType, RasterProperties,
34
    RasterPropertiesEntry, RasterPropertiesEntryType, RasterPropertiesKey, RasterTile2D,
35
    TilingStrategy,
36
};
37
use geoengine_datatypes::util::test::TestDefault;
38
use geoengine_datatypes::{dataset::DataId, raster::TileInformation};
39
use geoengine_datatypes::{
40
    primitives::TimeInterval,
41
    raster::{Grid, GridBlit, GridBoundingBox2D, GridIdx, GridSize, TilingSpecification},
42
};
43
pub use loading_info::{
44
    GdalLoadingInfo, GdalLoadingInfoTemporalSlice, GdalLoadingInfoTemporalSliceIterator,
45
    GdalMetaDataList, GdalMetaDataRegular, GdalMetaDataStatic, GdalMetadataNetCdfCf,
46
};
47
use log::debug;
48
use num::FromPrimitive;
49
use serde::{Deserialize, Serialize};
50
use snafu::{ensure, ResultExt};
51
use std::collections::HashMap;
52
use std::convert::TryFrom;
53
use std::ffi::CString;
54
use std::marker::PhantomData;
55
use std::path::{Path, PathBuf};
56
use std::time::Instant;
57
use tracing::{span, Level};
58

59
mod error;
60
mod loading_info;
61

62
static GDAL_RETRY_INITIAL_BACKOFF_MS: u64 = 1000;
63
static GDAL_RETRY_MAX_BACKOFF_MS: u64 = 60 * 60 * 1000;
64
static GDAL_RETRY_EXPONENTIAL_BACKOFF_FACTOR: f64 = 2.;
65

66
/// Parameters for the GDAL Source Operator
67
///
68
/// # Examples
69
///
70
/// ```rust
71
/// use serde_json::{Result, Value};
72
/// use geoengine_operators::source::{GdalSource, GdalSourceParameters};
73
/// use geoengine_datatypes::dataset::{DatasetId, DataId};
74
/// use geoengine_datatypes::util::Identifier;
75
/// use std::str::FromStr;
76
///
77
/// let json_string = r#"
78
///     {
79
///         "type": "GdalSource",
80
///         "params": {
81
///             "data": {
82
///                 "type": "internal",
83
///                 "datasetId": "a626c880-1c41-489b-9e19-9596d129859c"
84
///             }
85
///         }
86
///     }"#;
87
///
88
/// let operator: GdalSource = serde_json::from_str(json_string).unwrap();
89
///
90
/// assert_eq!(operator, GdalSource {
91
///     params: GdalSourceParameters {
92
///         data: DatasetId::from_str("a626c880-1c41-489b-9e19-9596d129859c").unwrap().into()
93
///     },
94
/// });
95
/// ```
96
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
72✔
97
pub struct GdalSourceParameters {
98
    pub data: DataId,
99
}
100

101
impl OperatorData for GdalSourceParameters {
102
    fn data_ids_collect(&self, data_ids: &mut Vec<DataId>) {
2✔
103
        data_ids.push(self.data.clone());
2✔
104
    }
2✔
105
}
106

107
type GdalMetaData =
108
    Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>>;
109

110
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209✔
111
#[serde(rename_all = "camelCase")]
112
pub struct GdalSourceTimePlaceholder {
113
    pub format: DateTimeParseFormat,
114
    pub reference: TimeReference,
115
}
116

117
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
209✔
118
#[serde(rename_all = "camelCase")]
119
pub enum TimeReference {
120
    Start,
121
    End,
122
}
123

124
/// Parameters for loading data using Gdal
125
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone)]
1,513✔
126
#[serde(rename_all = "camelCase")]
127
pub struct GdalDatasetParameters {
128
    pub file_path: PathBuf,
129
    pub rasterband_channel: usize,
130
    pub geo_transform: GdalDatasetGeoTransform, // TODO: discuss if we need this at all
131
    pub width: usize,
132
    pub height: usize,
133
    pub file_not_found_handling: FileNotFoundHandling,
134
    #[serde(default)]
135
    #[serde(with = "float_option_with_nan")]
136
    pub no_data_value: Option<f64>,
137
    pub properties_mapping: Option<Vec<GdalMetadataMapping>>,
138
    // Dataset open option as strings, e.g. `vec!["UserPwd=geoengine:pwd".to_owned(), "HttpAuth=BASIC".to_owned()]`
139
    pub gdal_open_options: Option<Vec<String>>,
140
    // Configs as key, value pairs that will be set as thread local config options, e.g.
141
    // `vec!["AWS_REGION".to_owned(), "eu-central-1".to_owned()]` and unset afterwards
142
    // TODO: validate the config options: only allow specific keys and specific values
143
    pub gdal_config_options: Option<Vec<(String, String)>>,
144
    #[serde(default)]
145
    pub allow_alphaband_as_mask: bool,
146
    pub retry: Option<GdalRetryOptions>,
147
}
148

149
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Copy)]
15✔
150
#[serde(rename_all = "camelCase")]
151
pub struct GdalRetryOptions {
152
    pub max_retries: usize,
153
}
154

155
#[derive(Debug, PartialEq, Eq)]
2✔
156
struct GdalReadWindow {
157
    read_start_x: isize, // pixelspace origin
158
    read_start_y: isize,
159
    read_size_x: usize, // pixelspace size
160
    read_size_y: usize,
161
}
162

163
impl GdalReadWindow {
164
    fn gdal_window_start(&self) -> (isize, isize) {
538✔
165
        (self.read_start_x, self.read_start_y)
538✔
166
    }
538✔
167

168
    fn gdal_window_size(&self) -> (usize, usize) {
538✔
169
        (self.read_size_x, self.read_size_y)
538✔
170
    }
538✔
171
}
172

173
/// A user friendly representation of Gdal's geo transform. In contrast to [`GeoTransform`] this
174
/// geo transform allows arbitrary pixel sizes and can thus also represent rasters where the origin is not located
175
/// in the upper left corner. It should only be used for loading rasters with Gdal and not internally.
176
/// The GDAL pixel space is usually anchored at the "top-left" corner of the data spatial bounds. Therefore the raster data is stored with spatial coordinate y-values decreasing with the rasters rows. This is represented by a negative pixel size.
177
/// However, there are datasets where the data is stored "upside-down". If this is the case, the pixel size is positive.
178
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
1,513✔
179
#[serde(rename_all = "camelCase")]
180
pub struct GdalDatasetGeoTransform {
181
    pub origin_coordinate: Coordinate2D,
182
    pub x_pixel_size: f64,
183
    pub y_pixel_size: f64,
184
}
185

186
impl GdalDatasetGeoTransform {
187
    /// Produce the `SpatialPartition` anchored at the datasets origin with a size of x * y pixels. This method handles non-standard pixel sizes.
188
    pub fn spatial_partition(&self, x_size: usize, y_size: usize) -> SpatialPartition2D {
1,112✔
189
        // the opposite y value (y value of the non origin edge)
1,112✔
190
        let opposite_coord_y = self.origin_coordinate.y + self.y_pixel_size * y_size as f64;
1,112✔
191

192
        // if the y-axis is negative then the origin is on the upper side.
193
        let (upper_y, lower_y) = if self.y_pixel_size.is_sign_negative() {
1,112✔
194
            (self.origin_coordinate.y, opposite_coord_y)
1,109✔
195
        } else {
196
            (opposite_coord_y, self.origin_coordinate.y)
3✔
197
        };
198

199
        let opposite_coord_x = self.origin_coordinate.x + self.x_pixel_size * x_size as f64;
1,112✔
200

201
        // if the y-axis is negative then the origin is on the upper side.
202
        let (left_x, right_x) = if self.x_pixel_size.is_sign_positive() {
1,112✔
203
            (self.origin_coordinate.x, opposite_coord_x)
1,112✔
204
        } else {
205
            (opposite_coord_x, self.origin_coordinate.x)
×
206
        };
207

208
        SpatialPartition2D::new_unchecked(
1,112✔
209
            Coordinate2D::new(left_x, upper_y),
1,112✔
210
            Coordinate2D::new(right_x, lower_y),
1,112✔
211
        )
1,112✔
212
    }
1,112✔
213

214
    /// Transform a `Coordinate2D` into a `GridIdx2D`
215
    #[inline]
216
    pub fn coordinate_to_grid_idx_2d(&self, coord: Coordinate2D) -> GridIdx2D {
1,063✔
217
        // TODO: use an epsilon here?
1,063✔
218
        let grid_x_index =
1,063✔
219
            ((coord.x - self.origin_coordinate.x) / self.x_pixel_size).floor() as isize;
1,063✔
220
        let grid_y_index =
1,063✔
221
            ((coord.y - self.origin_coordinate.y) / self.y_pixel_size).floor() as isize;
1,063✔
222

1,063✔
223
        [grid_y_index, grid_x_index].into()
1,063✔
224
    }
1,063✔
225

226
    fn spatial_partition_to_read_window(
532✔
227
        &self,
532✔
228
        spatial_partition: &SpatialPartition2D,
532✔
229
    ) -> GdalReadWindow {
532✔
230
        // World coordinates and pixel sizes use float values. Since the float imprecision might cause overflowing into the next pixel we use an epsilon to correct values very close the pixel borders. This logic is the same as used in [`GeoTransform::grid_idx_to_pixel_upper_left_coordinate_2d`].
532✔
231
        const EPSILON: f64 = 0.000_001;
532✔
232
        let epsilon: Coordinate2D =
532✔
233
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
532✔
234

235
        /*
236
        The read window is relative to the transform of the gdal dataset. The `SpatialPartition` is oriented at axis of the spatial SRS. This usually causes this situation:
237

238
        The gdal data is stored with negative pixel size. The "ul" coordinate of the `SpatialPartition` is neareest to the origin of the gdal raster data.
239
        ul                      ur
240
        +_______________________+
241
        |_|_ row 1              |
242
        | |_|_  row 2           |
243
        |   |_|_  row ...       |
244
        |     |_|               |
245
        |_______________________|
246
        +                       *
247
        ll                      lr
248

249
        However, sometimes the data is stored up-side down. Like this:
250

251
        The gdal data is stored with a positive pixel size. So the "ll" coordinate is nearest to the reading the raster data needs to starts at this anchor.
252

253
        ul                      ur
254
        +_______________________+
255
        |      _                |
256
        |    _|_|  row ...      |
257
        |  _|_|  row 3          |
258
        | |_|  row 2            |
259
        |_______________________|
260
        +                       *
261
        ll                      lr
262

263
        Therefore we need to select the raster read start based on the coordinate next to the raster data origin. From there we then calculate the size of the window to read.
264
        */
265
        let (near_origin_coord, far_origin_coord) = if self.y_pixel_size.is_sign_negative() {
532✔
266
            (
530✔
267
                spatial_partition.upper_left(),
530✔
268
                spatial_partition.lower_right(),
530✔
269
            )
530✔
270
        } else {
271
            (
2✔
272
                spatial_partition.lower_left(),
2✔
273
                spatial_partition.upper_right(),
2✔
274
            )
2✔
275
        };
276

277
        // Move the coordinate near the origin a bit inside the bbox by adding an epsilon of the pixel size.
278
        let safe_near_coord = near_origin_coord + epsilon;
532✔
279
        // Move the coordinate far from the origin a bit inside the bbox by subtracting an epsilon of the pixel size
532✔
280
        let safe_far_coord = far_origin_coord - epsilon;
532✔
281

532✔
282
        let GridIdx([near_idx_y, near_idx_x]) = self.coordinate_to_grid_idx_2d(safe_near_coord);
532✔
283
        let GridIdx([far_idx_y, far_idx_x]) = self.coordinate_to_grid_idx_2d(safe_far_coord);
532✔
284

285
        debug_assert!(near_idx_x <= far_idx_x);
532✔
286
        debug_assert!(near_idx_y <= far_idx_y);
532✔
287

288
        let read_size_x = (far_idx_x - near_idx_x) as usize + 1;
532✔
289
        let read_size_y = (far_idx_y - near_idx_y) as usize + 1;
532✔
290

532✔
291
        GdalReadWindow {
532✔
292
            read_start_x: near_idx_x,
532✔
293
            read_start_y: near_idx_y,
532✔
294
            read_size_x,
532✔
295
            read_size_y,
532✔
296
        }
532✔
297
    }
532✔
298
}
299

300
/// Default implementation for testing purposes where geo transform doesn't matter
301
impl TestDefault for GdalDatasetGeoTransform {
302
    fn test_default() -> Self {
15✔
303
        Self {
15✔
304
            origin_coordinate: (0.0, 0.0).into(),
15✔
305
            x_pixel_size: 1.0,
15✔
306
            y_pixel_size: -1.0,
15✔
307
        }
15✔
308
    }
15✔
309
}
310

311
impl ApproxEq for GdalDatasetGeoTransform {
312
    type Margin = float_cmp::F64Margin;
313

314
    fn approx_eq<M>(self, other: Self, margin: M) -> bool
529✔
315
    where
529✔
316
        M: Into<Self::Margin>,
529✔
317
    {
529✔
318
        let m = margin.into();
529✔
319
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
529✔
320
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
530✔
321
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
530✔
322
    }
530✔
323
}
324

325
/// Direct conversion from `GdalDatasetGeoTransform` to [`GeoTransform`] only works if origin is located in the upper left corner.
326
impl TryFrom<GdalDatasetGeoTransform> for GeoTransform {
327
    type Error = Error;
328

329
    fn try_from(dataset_geo_transform: GdalDatasetGeoTransform) -> Result<Self> {
×
330
        ensure!(
×
331
            dataset_geo_transform.x_pixel_size > 0.0 && dataset_geo_transform.y_pixel_size < 0.0,
×
332
            crate::error::GeoTransformOrigin
×
333
        );
334

335
        Ok(GeoTransform::new(
×
336
            dataset_geo_transform.origin_coordinate,
×
337
            dataset_geo_transform.x_pixel_size,
×
338
            dataset_geo_transform.y_pixel_size,
×
339
        ))
×
340
    }
×
341
}
342

343
impl From<gdal::GeoTransform> for GdalDatasetGeoTransform {
344
    fn from(gdal_geo_transform: gdal::GeoTransform) -> Self {
585✔
345
        Self {
585✔
346
            origin_coordinate: (gdal_geo_transform[0], gdal_geo_transform[3]).into(),
585✔
347
            x_pixel_size: gdal_geo_transform[1],
585✔
348
            y_pixel_size: gdal_geo_transform[5],
585✔
349
        }
585✔
350
    }
585✔
351
}
352

353
impl SpatialPartitioned for GdalDatasetParameters {
354
    fn spatial_partition(&self) -> SpatialPartition2D {
578✔
355
        self.geo_transform
578✔
356
            .spatial_partition(self.width, self.height)
578✔
357
    }
578✔
358
}
359

360
impl GridShapeAccess for GdalDatasetParameters {
361
    type ShapeArray = [usize; 2];
362

363
    fn grid_shape_array(&self) -> Self::ShapeArray {
×
364
        [self.height, self.width]
×
365
    }
×
366
}
367

368
/// How to handle file not found errors
369
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1,513✔
370
pub enum FileNotFoundHandling {
371
    NoData, // output tiles filled with nodata
372
    Error,  // return error tile
373
}
374

375
impl GdalDatasetParameters {
376
    /// Placeholders are replaced by formatted time value.
377
    /// E.g. `%my_placeholder%` could be replaced by `2014-04-01` depending on the format and time input.
378
    pub fn replace_time_placeholders(
108✔
379
        &self,
108✔
380
        placeholders: &HashMap<String, GdalSourceTimePlaceholder>,
108✔
381
        time: TimeInterval,
108✔
382
    ) -> Result<Self> {
108✔
383
        let mut file_path: String = self.file_path.to_string_lossy().into();
108✔
384

385
        for (placeholder, time_placeholder) in placeholders {
216✔
386
            let time = match time_placeholder.reference {
108✔
387
                TimeReference::Start => time.start(),
108✔
388
                TimeReference::End => time.end(),
×
389
            };
390
            let time_string = time
108✔
391
                .as_date_time()
108✔
392
                .ok_or(Error::TimeInstanceNotDisplayable)?
108✔
393
                .format(&time_placeholder.format);
108✔
394

108✔
395
            // TODO: use more efficient algorithm for replacing multiple placeholders, e.g. aho-corasick
108✔
396
            file_path = file_path.replace(placeholder, &time_string);
108✔
397
        }
398

399
        Ok(Self {
108✔
400
            file_not_found_handling: self.file_not_found_handling,
108✔
401
            file_path: file_path.into(),
108✔
402
            properties_mapping: self.properties_mapping.clone(),
108✔
403
            gdal_open_options: self.gdal_open_options.clone(),
108✔
404
            gdal_config_options: self.gdal_config_options.clone(),
108✔
405
            ..*self
108✔
406
        })
108✔
407
    }
108✔
408
}
409

410
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
×
411
pub struct TilingInformation {
412
    pub x_axis_tiles: usize,
413
    pub y_axis_tiles: usize,
414
    pub x_axis_tile_size: usize,
415
    pub y_axis_tile_size: usize,
416
}
417

418
pub struct GdalSourceProcessor<T>
419
where
420
    T: Pixel,
421
{
422
    pub tiling_specification: TilingSpecification,
423
    pub meta_data: GdalMetaData,
424
    pub _phantom_data: PhantomData<T>,
425
}
426

427
struct GdalRasterLoader {}
428

429
impl GdalRasterLoader {
430
    ///
431
    /// A method to async load single tiles from a GDAL dataset.
432
    ///
433
    async fn load_tile_data_async<T: Pixel + GdalType + FromPrimitive>(
524✔
434
        dataset_params: GdalDatasetParameters,
524✔
435
        tile_information: TileInformation,
524✔
436
        tile_time: TimeInterval,
524✔
437
    ) -> Result<RasterTile2D<T>> {
524✔
438
        // TODO: detect usage of vsi curl properly, e.g. also check for `/vsicurl_streaming` and combinations with `/vsizip`
524✔
439
        let is_vsi_curl = dataset_params.file_path.starts_with("/vsicurl/");
524✔
440

524✔
441
        retry(
524✔
442
            dataset_params
524✔
443
                .retry
524✔
444
                .map(|r| r.max_retries)
524✔
445
                .unwrap_or_default(),
524✔
446
            GDAL_RETRY_INITIAL_BACKOFF_MS,
524✔
447
            GDAL_RETRY_EXPONENTIAL_BACKOFF_FACTOR,
524✔
448
            Some(GDAL_RETRY_MAX_BACKOFF_MS),
524✔
449
            move || {
530✔
450
                let ds = dataset_params.clone();
530✔
451
                let file_path = ds.file_path.clone();
530✔
452

453
                async move {
530✔
454
                    let load_tile_result = crate::util::spawn_blocking(move || {
530✔
455
                        Self::load_tile_data(&ds, tile_information, tile_time)
530✔
456
                    })
530✔
457
                    .await
499✔
458
                    .context(crate::error::TokioJoin);
508✔
459

460
                    match load_tile_result {
508✔
461
                        Ok(Ok(r)) => Ok(r),
502✔
462
                        Ok(Err(e)) | Err(e) => {
6✔
463
                            if is_vsi_curl {
6✔
464
                                // clear the VSICurl cache, to force GDAL to try to re-download the file
6✔
465
                                // otherwise it will assume any observed error will happen again
6✔
466
                                clear_gdal_vsi_cache_for_path(file_path.as_path());
6✔
467
                            }
6✔
468

469
                            Err(e)
6✔
470
                        }
471
                    }
472
                }
508✔
473
            },
530✔
474
        )
524✔
475
        .await
505✔
476
    }
502✔
477

478
    async fn load_tile_async<T: Pixel + GdalType + FromPrimitive>(
638✔
479
        dataset_params: Option<GdalDatasetParameters>,
638✔
480
        tile_information: TileInformation,
638✔
481
        tile_time: TimeInterval,
638✔
482
    ) -> Result<RasterTile2D<T>> {
638✔
483
        match dataset_params {
578✔
484
            // TODO: discuss if we need this check here. The metadata provider should only pass on loading infos if the query intersects the datasets bounds! And the tiling strategy should only generate tiles that intersect the querys bbox.
485
            Some(ds)
524✔
486
                if tile_information
578✔
487
                    .spatial_partition()
578✔
488
                    .intersects(&ds.spatial_partition()) =>
578✔
489
            {
490
                debug!(
491
                    "Loading tile {:?}, from {:?}, band: {}",
×
492
                    &tile_information, ds.file_path, ds.rasterband_channel
×
493
                );
494
                Self::load_tile_data_async(ds, tile_information, tile_time).await
524✔
495
            }
496
            Some(_) => {
497
                debug!("Skipping tile not in query rect {:?}", &tile_information);
×
498

499
                Ok(create_no_data_tile(tile_information, tile_time))
54✔
500
            }
501

502
            _ => {
503
                debug!(
504
                    "Skipping tile without GdalDatasetParameters {:?}",
×
505
                    &tile_information
×
506
                );
507

508
                Ok(create_no_data_tile(tile_information, tile_time))
45✔
509
            }
510
        }
511
    }
601✔
512

513
    ///
514
    /// A method to load single tiles from a GDAL dataset.
515
    ///
516
    fn load_tile_data<T: Pixel + GdalType + FromPrimitive>(
538✔
517
        dataset_params: &GdalDatasetParameters,
538✔
518
        tile_information: TileInformation,
538✔
519
        tile_time: TimeInterval,
538✔
520
    ) -> Result<RasterTile2D<T>> {
538✔
521
        let start = Instant::now();
538✔
522

538✔
523
        debug!(
538✔
524
            "GridOrEmpty2D<{:?}> requested for {:?}.",
×
525
            T::TYPE,
×
526
            &tile_information.spatial_partition()
×
527
        );
528

529
        let options = dataset_params
538✔
530
            .gdal_open_options
538✔
531
            .as_ref()
538✔
532
            .map(|o| o.iter().map(String::as_str).collect::<Vec<_>>());
538✔
533

538✔
534
        // reverts the thread local configs on drop
538✔
535
        let _thread_local_configs = dataset_params
538✔
536
            .gdal_config_options
538✔
537
            .as_ref()
538✔
538
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
538✔
539

538✔
540
        let dataset_result = gdal_open_dataset_ex(
538✔
541
            &dataset_params.file_path,
538✔
542
            DatasetOptions {
538✔
543
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
538✔
544
                open_options: options.as_deref(),
538✔
545
                ..DatasetOptions::default()
538✔
546
            },
538✔
547
        );
538✔
548

549
        if let Err(error) = &dataset_result {
538✔
550
            let is_file_not_found = error_is_gdal_file_not_found(error);
7✔
551

552
            let err_result = match dataset_params.file_not_found_handling {
7✔
553
                FileNotFoundHandling::NoData if is_file_not_found => {
2✔
554
                    Ok(create_no_data_tile(tile_information, tile_time))
2✔
555
                }
556
                _ => Err(crate::error::Error::CouldNotOpenGdalDataset {
5✔
557
                    file_path: dataset_params.file_path.to_string_lossy().to_string(),
5✔
558
                }),
5✔
559
            };
560
            let elapsed = start.elapsed();
7✔
561
            debug!(
7✔
562
                "error opening dataset: {:?} -> returning error = {}, took: {:?}, file: {:?}",
×
563
                error,
×
564
                err_result.is_err(),
×
565
                elapsed,
566
                dataset_params.file_path
567
            );
568
            return err_result;
7✔
569
        };
531✔
570

531✔
571
        let dataset = dataset_result.expect("checked");
531✔
572

573
        let result_tile = read_raster_tile_with_properties(
531✔
574
            &dataset,
531✔
575
            dataset_params,
531✔
576
            tile_information,
531✔
577
            tile_time,
531✔
578
        )?;
531✔
579

580
        let elapsed = start.elapsed();
528✔
581
        debug!("data loaded -> returning data grid, took {:?}", elapsed);
528✔
582

583
        Ok(result_tile)
528✔
584
    }
538✔
585

586
    ///
587
    /// A stream of futures producing `RasterTile2D` for a single slice in time
588
    ///
589
    fn temporal_slice_tile_future_stream<T: Pixel + GdalType + FromPrimitive>(
119✔
590
        query: RasterQueryRectangle,
119✔
591
        info: GdalLoadingInfoTemporalSlice,
119✔
592
        tiling_strategy: TilingStrategy,
119✔
593
    ) -> impl Stream<Item = impl Future<Output = Result<RasterTile2D<T>>>> {
119✔
594
        stream::iter(tiling_strategy.tile_information_iterator(query.spatial_bounds)).map(
119✔
595
            move |tile| GdalRasterLoader::load_tile_async(info.params.clone(), tile, info.time),
637✔
596
        )
119✔
597
    }
119✔
598

599
    fn loading_info_to_tile_stream<
107✔
600
        T: Pixel + GdalType + FromPrimitive,
107✔
601
        S: Stream<Item = Result<GdalLoadingInfoTemporalSlice>>,
107✔
602
    >(
107✔
603
        loading_info_stream: S,
107✔
604
        query: RasterQueryRectangle,
107✔
605
        tiling_strategy: TilingStrategy,
107✔
606
    ) -> impl Stream<Item = Result<RasterTile2D<T>>> {
107✔
607
        loading_info_stream
107✔
608
            .map_ok(move |info| {
119✔
609
                GdalRasterLoader::temporal_slice_tile_future_stream(query, info, tiling_strategy)
119✔
610
                    .map(Result::Ok)
119✔
611
            })
119✔
612
            .try_flatten()
107✔
613
            .try_buffered(16) // TODO: make this configurable
107✔
614
    }
107✔
615
}
616

617
fn error_is_gdal_file_not_found(error: &Error) -> bool {
10✔
618
    matches!(
3✔
619
        error,
10✔
620
        Error::Gdal {
10✔
621
            source: GdalError::NullPointer {
10✔
622
                method_name,
10✔
623
                msg
10✔
624
            },
10✔
625
        } if *method_name == "GDALOpenEx" && (*msg == "HTTP response code: 404" || msg.ends_with("No such file or directory"))
10✔
626
    )
627
}
10✔
628

629
fn clear_gdal_vsi_cache_for_path(file_path: &Path) {
630
    unsafe {
631
        if let Some(Some(c_string)) = file_path.to_str().map(|s| CString::new(s).ok()) {
7✔
632
            VSICurlPartialClearCache(c_string.as_ptr());
7✔
633
        }
7✔
634
    }
635
}
7✔
636

637
impl<T> GdalSourceProcessor<T> where T: gdal::raster::GdalType + Pixel {}
638

639
#[async_trait]
640
impl<P> QueryProcessor for GdalSourceProcessor<P>
641
where
642
    P: Pixel + gdal::raster::GdalType + FromPrimitive,
643
{
644
    type Output = RasterTile2D<P>;
645
    type SpatialBounds = SpatialPartition2D;
646

647
    async fn _query<'a>(
107✔
648
        &'a self,
107✔
649
        query: RasterQueryRectangle,
107✔
650
        _ctx: &'a dyn crate::engine::QueryContext,
107✔
651
    ) -> Result<BoxStream<Result<Self::Output>>> {
107✔
652
        let start = Instant::now();
107✔
653
        debug!(
107✔
654
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
×
655
            P::TYPE,
×
656
            &query
×
657
        );
658

659
        debug!(
107✔
660
            "GdalSourceProcessor<{:?}> meta data loaded, took {:?}.",
×
661
            P::TYPE,
×
662
            start.elapsed()
×
663
        );
664

665
        let spatial_resolution = query.spatial_resolution;
107✔
666

107✔
667
        // A `GeoTransform` maps pixel space to world space.
107✔
668
        // Usually a SRS has axis directions pointing "up" (y-axis) and "up" (y-axis).
107✔
669
        // We are not aware of spatial reference systems where the x-axis points to the right.
107✔
670
        // However, there are spatial reference systems where the y-axis points downwards.
107✔
671
        // The standard "pixel-space" starts at the top-left corner of a `GeoTransform` and points down-right.
107✔
672
        // Therefore, the pixel size on the x-axis is always increasing
107✔
673
        let pixel_size_x = spatial_resolution.x;
107✔
674
        debug_assert!(pixel_size_x.is_sign_positive());
107✔
675
        // and the y-axis should only be positive if the y-axis of the spatial reference system also "points down".
676
        // NOTE: at the moment we do not allow "down pointing" y-axis.
677
        let pixel_size_y = spatial_resolution.y * -1.0;
107✔
678
        debug_assert!(pixel_size_y.is_sign_negative());
107✔
679

680
        let tiling_strategy = self
107✔
681
            .tiling_specification
107✔
682
            .strategy(pixel_size_x, pixel_size_y);
107✔
683

684
        let result_descriptor = self.meta_data.result_descriptor().await?;
107✔
685

686
        let mut empty = false;
107✔
687
        debug!("result descr bbox: {:?}", result_descriptor.bbox);
107✔
688
        debug!("query bbox: {:?}", query.spatial_bounds);
107✔
689

690
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
107✔
691
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
67✔
692
                debug!("query does not intersect spatial data bounds");
×
693
                empty = true;
×
694
            }
67✔
695
        }
40✔
696

697
        // TODO: use the time bounds to early return.
698
        /*
699
        if let Some(data_time_bounds) = result_descriptor.time {
700
            if !data_time_bounds.intersects(&query.time_interval) {
701
                debug!("query does not intersect temporal data bounds");
702
                empty = true;
703
            }
704
        }
705
        */
706

707
        let loading_iter = if empty {
107✔
708
            GdalLoadingInfoTemporalSliceIterator::Static {
×
709
                parts: vec![].into_iter(),
×
710
            }
×
711
        } else {
712
            let loading_info = self.meta_data.loading_info(query).await?;
107✔
713
            loading_info.info
107✔
714
        };
715

716
        let source_stream = stream::iter(loading_iter);
107✔
717

107✔
718
        let source_stream =
107✔
719
            GdalRasterLoader::loading_info_to_tile_stream(source_stream, query, tiling_strategy);
107✔
720

107✔
721
        // use SparseTilesFillAdapter to fill all the gaps
107✔
722
        let filled_stream = SparseTilesFillAdapter::new(
107✔
723
            source_stream,
107✔
724
            tiling_strategy.tile_grid_box(query.spatial_partition()),
107✔
725
            tiling_strategy.geo_transform,
107✔
726
            tiling_strategy.tile_size_in_pixels,
107✔
727
        );
107✔
728
        Ok(filled_stream.boxed())
107✔
729
    }
214✔
730
}
731

732
pub type GdalSource = SourceOperator<GdalSourceParameters>;
733

734
impl OperatorName for GdalSource {
735
    const TYPE_NAME: &'static str = "GdalSource";
736
}
737

738
#[typetag::serde]
10✔
739
#[async_trait]
740
impl RasterOperator for GdalSource {
741
    async fn _initialize(
44✔
742
        self: Box<Self>,
44✔
743
        context: &dyn crate::engine::ExecutionContext,
44✔
744
    ) -> Result<Box<dyn InitializedRasterOperator>> {
44✔
745
        let meta_data: GdalMetaData = context.meta_data(&self.params.data).await?;
44✔
746

747
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
44✔
748

749
        let op = InitializedGdalSourceOperator {
44✔
750
            result_descriptor: meta_data.result_descriptor().await?,
44✔
751
            meta_data,
44✔
752
            tiling_specification: context.tiling_specification(),
44✔
753
        };
44✔
754

44✔
755
        Ok(op.boxed())
44✔
756
    }
88✔
757

758
    span_fn!(GdalSource);
2✔
759
}
760

761
pub struct InitializedGdalSourceOperator {
762
    pub meta_data: GdalMetaData,
763
    pub result_descriptor: RasterResultDescriptor,
764
    pub tiling_specification: TilingSpecification,
765
}
766

767
impl InitializedRasterOperator for InitializedGdalSourceOperator {
768
    fn result_descriptor(&self) -> &RasterResultDescriptor {
77✔
769
        &self.result_descriptor
77✔
770
    }
77✔
771

772
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
38✔
773
        Ok(match self.result_descriptor().data_type {
38✔
774
            RasterDataType::U8 => TypedRasterQueryProcessor::U8(
35✔
775
                GdalSourceProcessor {
35✔
776
                    tiling_specification: self.tiling_specification,
35✔
777
                    meta_data: self.meta_data.clone(),
35✔
778
                    _phantom_data: PhantomData,
35✔
779
                }
35✔
780
                .boxed(),
35✔
781
            ),
35✔
782
            RasterDataType::U16 => TypedRasterQueryProcessor::U16(
2✔
783
                GdalSourceProcessor {
2✔
784
                    tiling_specification: self.tiling_specification,
2✔
785
                    meta_data: self.meta_data.clone(),
2✔
786
                    _phantom_data: PhantomData,
2✔
787
                }
2✔
788
                .boxed(),
2✔
789
            ),
2✔
790
            RasterDataType::U32 => TypedRasterQueryProcessor::U32(
×
791
                GdalSourceProcessor {
×
792
                    tiling_specification: self.tiling_specification,
×
793
                    meta_data: self.meta_data.clone(),
×
794
                    _phantom_data: PhantomData,
×
795
                }
×
796
                .boxed(),
×
797
            ),
×
798
            RasterDataType::U64 => {
799
                return Err(GdalSourceError::UnsupportedRasterType {
×
800
                    raster_type: RasterDataType::U64,
×
801
                })?
×
802
            }
803
            RasterDataType::I8 => {
804
                return Err(GdalSourceError::UnsupportedRasterType {
×
805
                    raster_type: RasterDataType::I8,
×
806
                })?
×
807
            }
808
            RasterDataType::I16 => TypedRasterQueryProcessor::I16(
1✔
809
                GdalSourceProcessor {
1✔
810
                    tiling_specification: self.tiling_specification,
1✔
811
                    meta_data: self.meta_data.clone(),
1✔
812
                    _phantom_data: PhantomData,
1✔
813
                }
1✔
814
                .boxed(),
1✔
815
            ),
1✔
816
            RasterDataType::I32 => TypedRasterQueryProcessor::I32(
×
817
                GdalSourceProcessor {
×
818
                    tiling_specification: self.tiling_specification,
×
819
                    meta_data: self.meta_data.clone(),
×
820
                    _phantom_data: PhantomData,
×
821
                }
×
822
                .boxed(),
×
823
            ),
×
824
            RasterDataType::I64 => {
825
                return Err(GdalSourceError::UnsupportedRasterType {
×
826
                    raster_type: RasterDataType::I64,
×
827
                })?
×
828
            }
829
            RasterDataType::F32 => TypedRasterQueryProcessor::F32(
×
830
                GdalSourceProcessor {
×
831
                    tiling_specification: self.tiling_specification,
×
832
                    meta_data: self.meta_data.clone(),
×
833
                    _phantom_data: PhantomData,
×
834
                }
×
835
                .boxed(),
×
836
            ),
×
837
            RasterDataType::F64 => TypedRasterQueryProcessor::F64(
×
838
                GdalSourceProcessor {
×
839
                    tiling_specification: self.tiling_specification,
×
840
                    meta_data: self.meta_data.clone(),
×
841
                    _phantom_data: PhantomData,
×
842
                }
×
843
                .boxed(),
×
844
            ),
×
845
        })
846
    }
38✔
847
}
848

849
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
850
/// It fails if the tile is not within the dataset.
851
#[allow(clippy::float_cmp)]
852
fn read_grid_from_raster<T, D>(
530✔
853
    rasterband: &GdalRasterBand,
530✔
854
    read_window: &GdalReadWindow,
530✔
855
    out_shape: D,
530✔
856
    dataset_params: &GdalDatasetParameters,
530✔
857
    flip_y_axis: bool,
530✔
858
) -> Result<GridOrEmpty<D, T>>
530✔
859
where
530✔
860
    T: Pixel + GdalType + Default + FromPrimitive,
530✔
861
    D: Clone + GridSize + PartialEq,
530✔
862
{
530✔
863
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
530✔
864

865
    let buffer = rasterband.read_as::<T>(
530✔
866
        read_window.gdal_window_start(), // pixelspace origin
530✔
867
        read_window.gdal_window_size(),  // pixelspace size
530✔
868
        gdal_out_shape,                  // requested raster size
530✔
869
        None,                            // sampling mode
530✔
870
    )?;
530✔
871
    let data_grid = Grid::new(out_shape.clone(), buffer.data)?;
528✔
872

873
    let data_grid = if flip_y_axis {
528✔
874
        data_grid.reversed_y_axis_grid()
1✔
875
    } else {
876
        data_grid
527✔
877
    };
878

879
    let dataset_mask_flags = rasterband.mask_flags()?;
528✔
880

881
    if dataset_mask_flags.is_all_valid() {
528✔
882
        debug!("all pixels are valid --> skip no-data and mask handling.");
1✔
883
        return Ok(MaskedGrid::new_with_data(data_grid).into());
1✔
884
    }
527✔
885

527✔
886
    if dataset_mask_flags.is_nodata() {
527✔
887
        debug!("raster uses a no-data value --> use no-data handling.");
519✔
888
        let no_data_value = dataset_params
519✔
889
            .no_data_value
519✔
890
            .or_else(|| rasterband.no_data_value())
519✔
891
            .and_then(FromPrimitive::from_f64);
519✔
892
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
519✔
893
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
519✔
894
        return Ok(grid_or_empty);
519✔
895
    }
8✔
896

8✔
897
    if dataset_mask_flags.is_alpha() {
8✔
898
        debug!("raster uses alpha band to mask pixels.");
×
899
        if !dataset_params.allow_alphaband_as_mask {
×
900
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
901
        }
×
902
    }
8✔
903

904
    debug!("use mask based no-data handling.");
8✔
905

906
    let mask_band = rasterband.open_mask_band()?;
8✔
907
    let mask_buffer = mask_band.read_as::<u8>(
8✔
908
        read_window.gdal_window_start(), // pixelspace origin
8✔
909
        read_window.gdal_window_size(),  // pixelspace size
8✔
910
        gdal_out_shape,                  // requested raster size
8✔
911
        None,                            // sampling mode
8✔
912
    )?;
8✔
913
    let mask_grid = Grid::new(out_shape, mask_buffer.data)?.map_elements(|p: u8| p > 0);
278,597✔
914

915
    let mask_grid = if flip_y_axis {
8✔
916
        mask_grid.reversed_y_axis_grid()
×
917
    } else {
918
        mask_grid
8✔
919
    };
920

921
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
8✔
922
    Ok(GridOrEmpty::from(masked_grid))
8✔
923
}
530✔
924

925
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
926
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
927
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
928
fn read_partial_grid_from_raster<T>(
292✔
929
    rasterband: &GdalRasterBand,
292✔
930
    gdal_read_window: &GdalReadWindow,
292✔
931
    out_tile_read_bounds: GridBoundingBox2D,
292✔
932
    out_tile_shape: GridShape2D,
292✔
933
    dataset_params: &GdalDatasetParameters,
292✔
934
    flip_y_axis: bool,
292✔
935
) -> Result<GridOrEmpty2D<T>>
292✔
936
where
292✔
937
    T: Pixel + GdalType + Default + FromPrimitive,
292✔
938
{
292✔
939
    let dataset_raster = read_grid_from_raster(
292✔
940
        rasterband,
292✔
941
        gdal_read_window,
292✔
942
        out_tile_read_bounds,
292✔
943
        dataset_params,
292✔
944
        flip_y_axis,
292✔
945
    )?;
292✔
946

947
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
290✔
948
    tile_raster.grid_blit_from(&dataset_raster);
290✔
949
    Ok(tile_raster)
290✔
950
}
292✔
951

952
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
953
/// It handles conversion to grid coordinates.
954
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
955
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
956
fn read_grid_and_handle_edges<T>(
530✔
957
    tile_info: TileInformation,
530✔
958
    dataset: &GdalDataset,
530✔
959
    rasterband: &GdalRasterBand,
530✔
960
    dataset_params: &GdalDatasetParameters,
530✔
961
) -> Result<GridOrEmpty2D<T>>
530✔
962
where
530✔
963
    T: Pixel + GdalType + Default + FromPrimitive,
530✔
964
{
530✔
965
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
530✔
966
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
530✔
967

530✔
968
    if !approx_eq!(
530✔
969
        GdalDatasetGeoTransform,
530✔
970
        gdal_dataset_geotransform,
530✔
971
        dataset_params.geo_transform
530✔
972
    ) {
530✔
973
        log::warn!(
×
974
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
975
            dataset_params.geo_transform,
976
            gdal_dataset_geotransform,
977
        );
978
    };
530✔
979

980
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
530✔
981
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
530✔
982

983
    let gdal_dataset_bounds =
530✔
984
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
530✔
985

530✔
986
    let output_bounds = tile_info.spatial_partition();
530✔
987
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
530✔
988
    let output_shape = tile_info.tile_size_in_pixels();
530✔
989

990
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
530✔
991
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
992
    };
993

994
    let tile_geo_transform = tile_info.tile_geo_transform();
530✔
995

530✔
996
    let gdal_read_window =
530✔
997
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
530✔
998

530✔
999
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
530✔
1000
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
530✔
1001

530✔
1002
    if is_y_axis_flipped {
530✔
1003
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1004
    }
529✔
1005

1006
    let result_grid = if dataset_intersection_area == output_bounds {
530✔
1007
        read_grid_from_raster(
238✔
1008
            rasterband,
238✔
1009
            &gdal_read_window,
238✔
1010
            output_shape,
238✔
1011
            dataset_params,
238✔
1012
            is_y_axis_flipped,
238✔
1013
        )?
238✔
1014
    } else {
1015
        let partial_tile_grid_bounds =
292✔
1016
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
292✔
1017

292✔
1018
        read_partial_grid_from_raster(
292✔
1019
            rasterband,
292✔
1020
            &gdal_read_window,
292✔
1021
            partial_tile_grid_bounds,
292✔
1022
            output_shape,
292✔
1023
            dataset_params,
292✔
1024
            is_y_axis_flipped,
292✔
1025
        )?
292✔
1026
    };
1027

1028
    Ok(result_grid)
528✔
1029
}
530✔
1030

1031
/// This method reads the data for a single tile with a specified size from the GDAL dataset and adds the requested metadata as properties to the tile.
1032
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
531✔
1033
    dataset: &GdalDataset,
531✔
1034
    dataset_params: &GdalDatasetParameters,
531✔
1035
    tile_info: TileInformation,
531✔
1036
    tile_time: TimeInterval,
531✔
1037
) -> Result<RasterTile2D<T>> {
531✔
1038
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
531✔
1039

1040
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
530✔
1041

1042
    let mut properties = RasterProperties::default();
528✔
1043

1044
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
528✔
1045
        properties_from_gdal(&mut properties, dataset, properties_mapping);
4✔
1046
        let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
4✔
1047
        properties_from_gdal(&mut properties, &rasterband, properties_mapping);
4✔
1048
        properties_from_band(&mut properties, &rasterband);
4✔
1049
    }
524✔
1050

1051
    Ok(RasterTile2D::new_with_tile_info_and_properties(
528✔
1052
        tile_time,
528✔
1053
        tile_info,
528✔
1054
        result_grid,
528✔
1055
        properties,
528✔
1056
    ))
528✔
1057
}
531✔
1058

1059
fn create_no_data_tile<T: Pixel>(
101✔
1060
    tile_info: TileInformation,
101✔
1061
    tile_time: TimeInterval,
101✔
1062
) -> RasterTile2D<T> {
101✔
1063
    RasterTile2D::new_with_tile_info_and_properties(
101✔
1064
        tile_time,
101✔
1065
        tile_info,
101✔
1066
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
101✔
1067
        RasterProperties::default(),
101✔
1068
    )
101✔
1069
}
101✔
1070

1071
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14✔
1072
pub struct GdalMetadataMapping {
1073
    pub source_key: RasterPropertiesKey,
1074
    pub target_key: RasterPropertiesKey,
1075
    pub target_type: RasterPropertiesEntryType,
1076
}
1077

1078
impl GdalMetadataMapping {
1079
    pub fn identity(
×
1080
        key: RasterPropertiesKey,
×
1081
        target_type: RasterPropertiesEntryType,
×
1082
    ) -> GdalMetadataMapping {
×
1083
        GdalMetadataMapping {
×
1084
            source_key: key.clone(),
×
1085
            target_key: key,
×
1086
            target_type,
×
1087
        }
×
1088
    }
×
1089
}
1090

1091
fn properties_from_gdal<'a, I, M>(
8✔
1092
    properties: &mut RasterProperties,
8✔
1093
    gdal_dataset: &M,
8✔
1094
    properties_mapping: I,
8✔
1095
) where
8✔
1096
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1097
    M: GdalMetadata,
8✔
1098
{
8✔
1099
    let mapping_iter = properties_mapping.into_iter();
8✔
1100

1101
    for m in mapping_iter {
24✔
1102
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1103
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1104
        } else {
1105
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1106
        };
1107

1108
        if let Some(d) = data {
16✔
1109
            let entry = match m.target_type {
8✔
1110
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1111
                    |_| RasterPropertiesEntry::String(d),
×
1112
                    RasterPropertiesEntry::Number,
×
1113
                ),
×
1114
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1115
            };
1116

1117
            debug!(
8✔
1118
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1119
                &m.source_key, &m.target_key, &entry
×
1120
            );
1121

1122
            properties
8✔
1123
                .properties_map
8✔
1124
                .insert(m.target_key.clone(), entry);
8✔
1125
        }
8✔
1126
    }
1127
}
8✔
1128

1129
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
1130
    if let Some(scale) = gdal_dataset.metadata_item("scale", "") {
4✔
1131
        properties.scale = scale.parse::<f64>().ok();
×
1132
    };
4✔
1133

1134
    if let Some(offset) = gdal_dataset.metadata_item("offset", "") {
4✔
1135
        properties.offset = offset.parse::<f64>().ok();
×
1136
    };
4✔
1137

1138
    if let Some(band_name) = gdal_dataset.metadata_item("band_name", "") {
4✔
1139
        properties.band_name = Some(band_name);
×
1140
    }
4✔
1141
}
4✔
1142

1143
#[cfg(test)]
1144
mod tests {
1145
    use super::*;
1146
    use crate::engine::{MockExecutionContext, MockQueryContext};
1147
    use crate::test_data;
1148
    use crate::util::gdal::add_ndvi_dataset;
1149
    use crate::util::Result;
1150

1151
    use geoengine_datatypes::hashmap;
1152
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1153
    use geoengine_datatypes::raster::{EmptyGrid2D, GridBounds, GridIdx2D};
1154
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1155
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1156
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1157
    use httptest::matchers::request;
1158
    use httptest::{responders, Expectation, Server};
1159

1160
    async fn query_gdal_source(
5✔
1161
        exe_ctx: &MockExecutionContext,
5✔
1162
        query_ctx: &MockQueryContext,
5✔
1163
        id: DataId,
5✔
1164
        output_shape: GridShape2D,
5✔
1165
        output_bounds: SpatialPartition2D,
5✔
1166
        time_interval: TimeInterval,
5✔
1167
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1168
        let op = GdalSource {
5✔
1169
            params: GdalSourceParameters { data: id.clone() },
5✔
1170
        }
5✔
1171
        .boxed();
5✔
1172

5✔
1173
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1174
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1175
        let spatial_resolution =
5✔
1176
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1177

1178
        let o = op.initialize(exe_ctx).await.unwrap();
5✔
1179

5✔
1180
        o.query_processor()
5✔
1181
            .unwrap()
5✔
1182
            .get_u8()
5✔
1183
            .unwrap()
5✔
1184
            .raster_query(
5✔
1185
                RasterQueryRectangle {
5✔
1186
                    spatial_bounds: output_bounds,
5✔
1187
                    time_interval,
5✔
1188
                    spatial_resolution,
5✔
1189
                },
5✔
1190
                query_ctx,
5✔
1191
            )
5✔
1192
            .await
×
1193
            .unwrap()
5✔
1194
            .collect()
5✔
1195
            .await
14✔
1196
    }
5✔
1197

1198
    fn load_ndvi_jan_2014(
3✔
1199
        output_shape: GridShape2D,
3✔
1200
        output_bounds: SpatialPartition2D,
3✔
1201
    ) -> Result<RasterTile2D<u8>> {
3✔
1202
        GdalRasterLoader::load_tile_data::<u8>(
3✔
1203
            &GdalDatasetParameters {
3✔
1204
                file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
3✔
1205
                rasterband_channel: 1,
3✔
1206
                geo_transform: GdalDatasetGeoTransform {
3✔
1207
                    origin_coordinate: (-180., 90.).into(),
3✔
1208
                    x_pixel_size: 0.1,
3✔
1209
                    y_pixel_size: -0.1,
3✔
1210
                },
3✔
1211
                width: 3600,
3✔
1212
                height: 1800,
3✔
1213
                file_not_found_handling: FileNotFoundHandling::NoData,
3✔
1214
                no_data_value: Some(0.),
3✔
1215
                properties_mapping: Some(vec![
3✔
1216
                    GdalMetadataMapping {
3✔
1217
                        source_key: RasterPropertiesKey {
3✔
1218
                            domain: None,
3✔
1219
                            key: "AREA_OR_POINT".to_string(),
3✔
1220
                        },
3✔
1221
                        target_type: RasterPropertiesEntryType::String,
3✔
1222
                        target_key: RasterPropertiesKey {
3✔
1223
                            domain: None,
3✔
1224
                            key: "AREA_OR_POINT".to_string(),
3✔
1225
                        },
3✔
1226
                    },
3✔
1227
                    GdalMetadataMapping {
3✔
1228
                        source_key: RasterPropertiesKey {
3✔
1229
                            domain: Some("IMAGE_STRUCTURE".to_string()),
3✔
1230
                            key: "COMPRESSION".to_string(),
3✔
1231
                        },
3✔
1232
                        target_type: RasterPropertiesEntryType::String,
3✔
1233
                        target_key: RasterPropertiesKey {
3✔
1234
                            domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
3✔
1235
                            key: "COMPRESSION".to_string(),
3✔
1236
                        },
3✔
1237
                    },
3✔
1238
                ]),
3✔
1239
                gdal_open_options: None,
3✔
1240
                gdal_config_options: None,
3✔
1241
                allow_alphaband_as_mask: true,
3✔
1242
                retry: None,
3✔
1243
            },
3✔
1244
            TileInformation::with_partition_and_shape(output_bounds, output_shape),
3✔
1245
            TimeInterval::default(),
3✔
1246
        )
3✔
1247
    }
3✔
1248

1249
    #[test]
1✔
1250
    fn tiling_strategy_origin() {
1✔
1251
        let tile_size_in_pixels = [600, 600];
1✔
1252
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1253
        let dataset_x_pixel_size = 0.1;
1✔
1254
        let dataset_y_pixel_size = -0.1;
1✔
1255
        let dataset_geo_transform = GeoTransform::new(
1✔
1256
            dataset_upper_right_coord,
1✔
1257
            dataset_x_pixel_size,
1✔
1258
            dataset_y_pixel_size,
1✔
1259
        );
1✔
1260

1✔
1261
        let partition = SpatialPartition2D::new((-180., 90.).into(), (180., -90.).into()).unwrap();
1✔
1262

1✔
1263
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1264
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1265
            geo_transform: dataset_geo_transform,
1✔
1266
        };
1✔
1267

1✔
1268
        assert_eq!(
1✔
1269
            origin_split_tileing_strategy
1✔
1270
                .geo_transform
1✔
1271
                .upper_left_pixel_idx(&partition),
1✔
1272
            [0, 0].into()
1✔
1273
        );
1✔
1274
        assert_eq!(
1✔
1275
            origin_split_tileing_strategy
1✔
1276
                .geo_transform
1✔
1277
                .lower_right_pixel_idx(&partition),
1✔
1278
            [1800 - 1, 3600 - 1].into()
1✔
1279
        );
1✔
1280

1281
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1282
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1283
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1284
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1285
    }
1✔
1286

1287
    #[test]
1✔
1288
    fn tiling_strategy_zero() {
1✔
1289
        let tile_size_in_pixels = [600, 600];
1✔
1290
        let dataset_x_pixel_size = 0.1;
1✔
1291
        let dataset_y_pixel_size = -0.1;
1✔
1292
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1293
            0.0,
1✔
1294
            dataset_x_pixel_size,
1✔
1295
            0.0,
1✔
1296
            dataset_y_pixel_size,
1✔
1297
        );
1✔
1298

1✔
1299
        let partition = SpatialPartition2D::new((-180., 90.).into(), (180., -90.).into()).unwrap();
1✔
1300

1✔
1301
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1302
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1303
            geo_transform: central_geo_transform,
1✔
1304
        };
1✔
1305

1✔
1306
        assert_eq!(
1✔
1307
            origin_split_tileing_strategy
1✔
1308
                .geo_transform
1✔
1309
                .upper_left_pixel_idx(&partition),
1✔
1310
            [-900, -1800].into()
1✔
1311
        );
1✔
1312
        assert_eq!(
1✔
1313
            origin_split_tileing_strategy
1✔
1314
                .geo_transform
1✔
1315
                .lower_right_pixel_idx(&partition),
1✔
1316
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1317
        );
1✔
1318

1319
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1320
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1321
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1322
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1323
    }
1✔
1324

1325
    #[test]
1✔
1326
    fn tile_idx_iterator() {
1✔
1327
        let tile_size_in_pixels = [600, 600];
1✔
1328
        let dataset_x_pixel_size = 0.1;
1✔
1329
        let dataset_y_pixel_size = -0.1;
1✔
1330
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1331
            0.0,
1✔
1332
            dataset_x_pixel_size,
1✔
1333
            0.0,
1✔
1334
            dataset_y_pixel_size,
1✔
1335
        );
1✔
1336

1✔
1337
        let partition = SpatialPartition2D::new((-180., 90.).into(), (180., -90.).into()).unwrap();
1✔
1338

1✔
1339
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1340
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1341
            geo_transform: central_geo_transform,
1✔
1342
        };
1✔
1343

1✔
1344
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1345
            .tile_idx_iterator(partition)
1✔
1346
            .collect();
1✔
1347
        assert_eq!(vres.len(), 4 * 6);
1✔
1348
        assert_eq!(vres[0], [-2, -3].into());
1✔
1349
        assert_eq!(vres[1], [-2, -2].into());
1✔
1350
        assert_eq!(vres[2], [-2, -1].into());
1✔
1351
        assert_eq!(vres[23], [1, 2].into());
1✔
1352
    }
1✔
1353

1354
    #[test]
1✔
1355
    fn tile_information_iterator() {
1✔
1356
        let tile_size_in_pixels = [600, 600];
1✔
1357
        let dataset_x_pixel_size = 0.1;
1✔
1358
        let dataset_y_pixel_size = -0.1;
1✔
1359

1✔
1360
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1361
            0.0,
1✔
1362
            dataset_x_pixel_size,
1✔
1363
            0.0,
1✔
1364
            dataset_y_pixel_size,
1✔
1365
        );
1✔
1366

1✔
1367
        let partition = SpatialPartition2D::new((-180., 90.).into(), (180., -90.).into()).unwrap();
1✔
1368

1✔
1369
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1370
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1371
            geo_transform: central_geo_transform,
1✔
1372
        };
1✔
1373

1✔
1374
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1375
            .tile_information_iterator(partition)
1✔
1376
            .collect();
1✔
1377
        assert_eq!(vres.len(), 4 * 6);
1✔
1378
        assert_eq!(
1✔
1379
            vres[0],
1✔
1380
            TileInformation::new(
1✔
1381
                [-2, -3].into(),
1✔
1382
                tile_size_in_pixels.into(),
1✔
1383
                central_geo_transform,
1✔
1384
            )
1✔
1385
        );
1✔
1386
        assert_eq!(
1✔
1387
            vres[1],
1✔
1388
            TileInformation::new(
1✔
1389
                [-2, -2].into(),
1✔
1390
                tile_size_in_pixels.into(),
1✔
1391
                central_geo_transform,
1✔
1392
            )
1✔
1393
        );
1✔
1394
        assert_eq!(
1✔
1395
            vres[12],
1✔
1396
            TileInformation::new(
1✔
1397
                [0, -3].into(),
1✔
1398
                tile_size_in_pixels.into(),
1✔
1399
                central_geo_transform,
1✔
1400
            )
1✔
1401
        );
1✔
1402
        assert_eq!(
1✔
1403
            vres[23],
1✔
1404
            TileInformation::new(
1✔
1405
                [1, 2].into(),
1✔
1406
                tile_size_in_pixels.into(),
1✔
1407
                central_geo_transform,
1✔
1408
            )
1✔
1409
        );
1✔
1410
    }
1✔
1411

1412
    #[test]
1✔
1413
    fn replace_time_placeholder() {
1✔
1414
        let params = GdalDatasetParameters {
1✔
1415
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1416
            rasterband_channel: 0,
1✔
1417
            geo_transform: TestDefault::test_default(),
1✔
1418
            width: 360,
1✔
1419
            height: 180,
1✔
1420
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1421
            no_data_value: Some(0.),
1✔
1422
            properties_mapping: None,
1✔
1423
            gdal_open_options: None,
1✔
1424
            gdal_config_options: None,
1✔
1425
            allow_alphaband_as_mask: true,
1✔
1426
            retry: None,
1✔
1427
        };
1✔
1428
        let replaced = params
1✔
1429
            .replace_time_placeholders(
1✔
1430
                &hashmap! {
1✔
1431
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1432
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1433
                        reference: TimeReference::Start,
1✔
1434
                    },
1✔
1435
                },
1✔
1436
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1437
            )
1✔
1438
            .unwrap();
1✔
1439
        assert_eq!(
1✔
1440
            replaced.file_path.to_string_lossy(),
1✔
1441
            "/foo/bar_022000000.tiff".to_string()
1✔
1442
        );
1✔
1443
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1444
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1445
        assert_eq!(params.width, replaced.width);
1✔
1446
        assert_eq!(params.height, replaced.height);
1✔
1447
        assert_eq!(
1✔
1448
            params.file_not_found_handling,
1✔
1449
            replaced.file_not_found_handling
1✔
1450
        );
1✔
1451
    }
1✔
1452

1453
    #[test]
1✔
1454
    fn test_load_tile_data() {
1✔
1455
        let output_shape: GridShape2D = [8, 8].into();
1✔
1456
        let output_bounds =
1✔
1457
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1458

1✔
1459
        let RasterTile2D {
1✔
1460
            global_geo_transform: _,
1✔
1461
            grid_array: grid,
1✔
1462
            tile_position: _,
1✔
1463
            time: _,
1✔
1464
            properties,
1✔
1465
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1466

1✔
1467
        assert!(!grid.is_empty());
1✔
1468

1469
        let grid = grid.into_materialized_masked_grid();
1✔
1470

1✔
1471
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1472
        assert_eq!(
1✔
1473
            grid.inner_grid.data,
1✔
1474
            &[
1✔
1475
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1476
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1477
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1478
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1479
            ]
1✔
1480
        );
1✔
1481

1482
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1483
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1484

1485
        assert!(properties.scale.is_none());
1✔
1486
        assert!(properties.offset.is_none());
1✔
1487
        assert_eq!(
1✔
1488
            properties.properties_map.get(&RasterPropertiesKey {
1✔
1489
                key: "AREA_OR_POINT".to_string(),
1✔
1490
                domain: None,
1✔
1491
            }),
1✔
1492
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1493
        );
1✔
1494
        assert_eq!(
1✔
1495
            properties.properties_map.get(&RasterPropertiesKey {
1✔
1496
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1497
                key: "COMPRESSION".to_string(),
1✔
1498
            }),
1✔
1499
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1500
        );
1✔
1501
    }
1✔
1502

1503
    #[test]
1✔
1504
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1505
        let output_shape: GridShape2D = [8, 8].into();
1✔
1506
        // shift world bbox one pixel up and to the left
1✔
1507
        let (x_size, y_size) = (45., 22.5);
1✔
1508
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1509
            (-180. - x_size, 90. + y_size).into(),
1✔
1510
            (180. - x_size, -90. + y_size).into(),
1✔
1511
        );
1✔
1512

1✔
1513
        let RasterTile2D {
1✔
1514
            global_geo_transform: _,
1✔
1515
            grid_array: grid,
1✔
1516
            tile_position: _,
1✔
1517
            time: _,
1✔
1518
            properties: _,
1✔
1519
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1520

1✔
1521
        assert!(!grid.is_empty());
1✔
1522

1523
        let x = grid.into_materialized_masked_grid();
1✔
1524

1✔
1525
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1526
        assert_eq!(
1✔
1527
            x.inner_grid.data,
1✔
1528
            &[
1✔
1529
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1530
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1531
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1532
                255, 255, 255, 255, 255, 255
1✔
1533
            ]
1✔
1534
        );
1✔
1535
    }
1✔
1536

1537
    #[test]
1✔
1538
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1539
        let output_shape: GridShape2D = [8, 8].into();
1✔
1540
        // shift world bbox one pixel up and to the left
1✔
1541
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1542
        let output_bounds = SpatialPartition2D::new(
1✔
1543
            (-116.22222, 66.66666).into(),
1✔
1544
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1545
        )
1✔
1546
        .unwrap();
1✔
1547

1✔
1548
        let RasterTile2D {
1✔
1549
            global_geo_transform: _,
1✔
1550
            grid_array: grid,
1✔
1551
            tile_position: _,
1✔
1552
            time: _,
1✔
1553
            properties: _,
1✔
1554
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1555

1✔
1556
        assert!(!grid.is_empty());
1✔
1557

1558
        let x = grid.into_materialized_masked_grid();
1✔
1559

1✔
1560
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1561
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1562
    }
1✔
1563

1564
    #[tokio::test]
1✔
1565
    async fn test_query_single_time_slice() {
1✔
1566
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1567
        let query_ctx = MockQueryContext::test_default();
1✔
1568
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1569

1✔
1570
        let output_shape: GridShape2D = [256, 256].into();
1✔
1571
        let output_bounds =
1✔
1572
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1573
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001); // 2014-01-01
1✔
1574

1575
        let c = query_gdal_source(
1✔
1576
            &exe_ctx,
1✔
1577
            &query_ctx,
1✔
1578
            id,
1✔
1579
            output_shape,
1✔
1580
            output_bounds,
1✔
1581
            time_interval,
1✔
1582
        )
1✔
1583
        .await;
5✔
1584
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1585

1✔
1586
        assert_eq!(c.len(), 4);
1✔
1587

1588
        assert_eq!(
1✔
1589
            c[0].time,
1✔
1590
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1591
        );
1✔
1592

1593
        assert_eq!(
1✔
1594
            c[0].tile_information().global_tile_position(),
1✔
1595
            [-1, -1].into()
1✔
1596
        );
1✔
1597

1598
        assert_eq!(
1✔
1599
            c[1].tile_information().global_tile_position(),
1✔
1600
            [-1, 0].into()
1✔
1601
        );
1✔
1602

1603
        assert_eq!(
1✔
1604
            c[2].tile_information().global_tile_position(),
1✔
1605
            [0, -1].into()
1✔
1606
        );
1✔
1607

1608
        assert_eq!(
1✔
1609
            c[3].tile_information().global_tile_position(),
1✔
1610
            [0, 0].into()
1✔
1611
        );
1✔
1612
    }
1613

1614
    #[tokio::test]
1✔
1615
    async fn test_query_multi_time_slices() {
1✔
1616
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1617
        let query_ctx = MockQueryContext::test_default();
1✔
1618
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1619

1✔
1620
        let output_shape: GridShape2D = [256, 256].into();
1✔
1621
        let output_bounds =
1✔
1622
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1623
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-01
1✔
1624

1625
        let c = query_gdal_source(
1✔
1626
            &exe_ctx,
1✔
1627
            &query_ctx,
1✔
1628
            id,
1✔
1629
            output_shape,
1✔
1630
            output_bounds,
1✔
1631
            time_interval,
1✔
1632
        )
1✔
1633
        .await;
9✔
1634
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1635

1✔
1636
        assert_eq!(c.len(), 8);
1✔
1637

1638
        assert_eq!(
1✔
1639
            c[0].time,
1✔
1640
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1641
        );
1✔
1642

1643
        assert_eq!(
1✔
1644
            c[5].time,
1✔
1645
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1646
        );
1✔
1647
    }
1648

1649
    #[tokio::test]
1✔
1650
    async fn test_query_before_data() {
1✔
1651
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1652
        let query_ctx = MockQueryContext::test_default();
1✔
1653
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1654

1✔
1655
        let output_shape: GridShape2D = [256, 256].into();
1✔
1656
        let output_bounds =
1✔
1657
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1658
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1659

1660
        let c = query_gdal_source(
1✔
1661
            &exe_ctx,
1✔
1662
            &query_ctx,
1✔
1663
            id,
1✔
1664
            output_shape,
1✔
1665
            output_bounds,
1✔
1666
            time_interval,
1✔
1667
        )
1✔
1668
        .await;
×
1669
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1670

1✔
1671
        assert_eq!(c.len(), 4);
1✔
1672

1673
        assert_eq!(
1✔
1674
            c[0].time,
1✔
1675
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1676
        );
1✔
1677
    }
1678

1679
    #[tokio::test]
1✔
1680
    async fn test_query_after_data() {
1✔
1681
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1682
        let query_ctx = MockQueryContext::test_default();
1✔
1683
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1684

1✔
1685
        let output_shape: GridShape2D = [256, 256].into();
1✔
1686
        let output_bounds =
1✔
1687
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1688
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1689

1690
        let c = query_gdal_source(
1✔
1691
            &exe_ctx,
1✔
1692
            &query_ctx,
1✔
1693
            id,
1✔
1694
            output_shape,
1✔
1695
            output_bounds,
1✔
1696
            time_interval,
1✔
1697
        )
1✔
1698
        .await;
×
1699
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1700

1✔
1701
        assert_eq!(c.len(), 4);
1✔
1702

1703
        assert_eq!(
1✔
1704
            c[0].time,
1✔
1705
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1706
        );
1✔
1707
    }
1708

1709
    #[tokio::test]
1✔
1710
    async fn test_nodata() {
1✔
1711
        hide_gdal_errors();
1✔
1712

1✔
1713
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1714
        let query_ctx = MockQueryContext::test_default();
1✔
1715
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1716

1✔
1717
        let output_shape: GridShape2D = [256, 256].into();
1✔
1718
        let output_bounds =
1✔
1719
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1720
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1721

1722
        let c = query_gdal_source(
1✔
1723
            &exe_ctx,
1✔
1724
            &query_ctx,
1✔
1725
            id,
1✔
1726
            output_shape,
1✔
1727
            output_bounds,
1✔
1728
            time_interval,
1✔
1729
        )
1✔
1730
        .await;
×
1731
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1732

1✔
1733
        assert_eq!(c.len(), 4);
1✔
1734

1735
        let tile_1 = &c[0];
1✔
1736

1✔
1737
        assert_eq!(
1✔
1738
            tile_1.time,
1✔
1739
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1740
        );
1✔
1741

1742
        assert!(tile_1.is_empty());
1✔
1743
    }
1744

1745
    #[tokio::test]
1✔
1746
    async fn timestep_without_params() {
1✔
1747
        let output_bounds =
1✔
1748
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1749
        let output_shape: GridShape2D = [256, 256].into();
1✔
1750

1✔
1751
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1752
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1753
        let params = None;
1✔
1754

1755
        let tile = GdalRasterLoader::load_tile_async::<f64>(params, tile_info, time_interval).await;
1✔
1756

1757
        assert!(tile.is_ok());
1✔
1758

1759
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1760
            time_interval,
1✔
1761
            tile_info,
1✔
1762
            EmptyGrid2D::new(output_shape).into(),
1✔
1763
        );
1✔
1764

1✔
1765
        assert_eq!(tile.unwrap(), expected);
1✔
1766
    }
1767

1768
    #[test]
1✔
1769
    fn it_reverts_config_options() {
1✔
1770
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1771

1✔
1772
        {
1✔
1773
            let _config =
1✔
1774
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1775

1✔
1776
            assert_eq!(
1✔
1777
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1778
                "bar".to_owned()
1✔
1779
            );
1✔
1780
        }
1781

1782
        assert_eq!(
1✔
1783
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1784
            String::new()
1✔
1785
        );
1✔
1786
    }
1✔
1787

1788
    #[test]
1✔
1789
    #[allow(clippy::too_many_lines)]
1790
    fn deserialize_dataset_parameters() {
1✔
1791
        let dataset_parameters = GdalDatasetParameters {
1✔
1792
            file_path: "path-to-data.tiff".into(),
1✔
1793
            rasterband_channel: 1,
1✔
1794
            geo_transform: GdalDatasetGeoTransform {
1✔
1795
                origin_coordinate: (-180., 90.).into(),
1✔
1796
                x_pixel_size: 0.1,
1✔
1797
                y_pixel_size: -0.1,
1✔
1798
            },
1✔
1799
            width: 3600,
1✔
1800
            height: 1800,
1✔
1801
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1802
            no_data_value: Some(f64::NAN),
1✔
1803
            properties_mapping: Some(vec![
1✔
1804
                GdalMetadataMapping {
1✔
1805
                    source_key: RasterPropertiesKey {
1✔
1806
                        domain: None,
1✔
1807
                        key: "AREA_OR_POINT".to_string(),
1✔
1808
                    },
1✔
1809
                    target_type: RasterPropertiesEntryType::String,
1✔
1810
                    target_key: RasterPropertiesKey {
1✔
1811
                        domain: None,
1✔
1812
                        key: "AREA_OR_POINT".to_string(),
1✔
1813
                    },
1✔
1814
                },
1✔
1815
                GdalMetadataMapping {
1✔
1816
                    source_key: RasterPropertiesKey {
1✔
1817
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
1818
                        key: "COMPRESSION".to_string(),
1✔
1819
                    },
1✔
1820
                    target_type: RasterPropertiesEntryType::String,
1✔
1821
                    target_key: RasterPropertiesKey {
1✔
1822
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1823
                        key: "COMPRESSION".to_string(),
1✔
1824
                    },
1✔
1825
                },
1✔
1826
            ]),
1✔
1827
            gdal_open_options: None,
1✔
1828
            gdal_config_options: None,
1✔
1829
            allow_alphaband_as_mask: true,
1✔
1830
            retry: None,
1✔
1831
        };
1✔
1832

1✔
1833
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1834

1✔
1835
        assert_eq!(
1✔
1836
            dataset_parameters_json,
1✔
1837
            serde_json::json!({
1✔
1838
                "filePath": "path-to-data.tiff",
1✔
1839
                "rasterbandChannel": 1,
1✔
1840
                "geoTransform": {
1✔
1841
                    "originCoordinate": {
1✔
1842
                        "x": -180.,
1✔
1843
                        "y": 90.
1✔
1844
                    },
1✔
1845
                    "xPixelSize": 0.1,
1✔
1846
                    "yPixelSize": -0.1
1✔
1847
                },
1✔
1848
                "width": 3600,
1✔
1849
                "height": 1800,
1✔
1850
                "fileNotFoundHandling": "NoData",
1✔
1851
                "noDataValue": "nan",
1✔
1852
                "propertiesMapping": [{
1✔
1853
                        "source_key": {
1✔
1854
                            "domain": null,
1✔
1855
                            "key": "AREA_OR_POINT"
1✔
1856
                        },
1✔
1857
                        "target_key": {
1✔
1858
                            "domain": null,
1✔
1859
                            "key": "AREA_OR_POINT"
1✔
1860
                        },
1✔
1861
                        "target_type": "String"
1✔
1862
                    },
1✔
1863
                    {
1✔
1864
                        "source_key": {
1✔
1865
                            "domain": "IMAGE_STRUCTURE",
1✔
1866
                            "key": "COMPRESSION"
1✔
1867
                        },
1✔
1868
                        "target_key": {
1✔
1869
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1870
                            "key": "COMPRESSION"
1✔
1871
                        },
1✔
1872
                        "target_type": "String"
1✔
1873
                    }
1✔
1874
                ],
1✔
1875
                "gdalOpenOptions": null,
1✔
1876
                "gdalConfigOptions": null,
1✔
1877
                "allowAlphabandAsMask": true,
1✔
1878
                "retry": null,
1✔
1879
            })
1✔
1880
        );
1✔
1881

1882
        let deserialized_parameters =
1✔
1883
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1884

1✔
1885
        // since there is NaN in the data, we can't check for equality on the whole object
1✔
1886

1✔
1887
        assert_eq!(
1✔
1888
            deserialized_parameters.file_path,
1✔
1889
            dataset_parameters.file_path,
1✔
1890
        );
1✔
1891
        assert_eq!(
1✔
1892
            deserialized_parameters.rasterband_channel,
1✔
1893
            dataset_parameters.rasterband_channel,
1✔
1894
        );
1✔
1895
        assert_eq!(
1✔
1896
            deserialized_parameters.geo_transform,
1✔
1897
            dataset_parameters.geo_transform,
1✔
1898
        );
1✔
1899
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
1900
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
1901
        assert_eq!(
1✔
1902
            deserialized_parameters.file_not_found_handling,
1✔
1903
            dataset_parameters.file_not_found_handling,
1✔
1904
        );
1✔
1905
        assert!(
1✔
1906
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
1907
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
1908
        );
1909
        assert_eq!(
1✔
1910
            deserialized_parameters.properties_mapping,
1✔
1911
            dataset_parameters.properties_mapping,
1✔
1912
        );
1✔
1913
        assert_eq!(
1✔
1914
            deserialized_parameters.gdal_open_options,
1✔
1915
            dataset_parameters.gdal_open_options,
1✔
1916
        );
1✔
1917
        assert_eq!(
1✔
1918
            deserialized_parameters.gdal_config_options,
1✔
1919
            dataset_parameters.gdal_config_options,
1✔
1920
        );
1✔
1921
    }
1✔
1922

1923
    #[test]
1✔
1924
    fn gdal_geotransform_to_bounds_neg_y_0() {
1✔
1925
        let gt = GdalDatasetGeoTransform {
1✔
1926
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1927
            x_pixel_size: 1.,
1✔
1928
            y_pixel_size: -1.,
1✔
1929
        };
1✔
1930

1✔
1931
        let sb = gt.spatial_partition(10, 10);
1✔
1932

1✔
1933
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
1934
            .unwrap();
1✔
1935

1✔
1936
        assert_eq!(sb, exp);
1✔
1937
    }
1✔
1938

1939
    #[test]
1✔
1940
    fn gdal_geotransform_to_bounds_neg_y_5() {
1✔
1941
        let gt = GdalDatasetGeoTransform {
1✔
1942
            origin_coordinate: Coordinate2D::new(5., 5.),
1✔
1943
            x_pixel_size: 0.5,
1✔
1944
            y_pixel_size: -0.5,
1✔
1945
        };
1✔
1946

1✔
1947
        let sb = gt.spatial_partition(10, 10);
1✔
1948

1✔
1949
        let exp =
1✔
1950
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1✔
1951

1✔
1952
        assert_eq!(sb, exp);
1✔
1953
    }
1✔
1954

1955
    #[test]
1✔
1956
    fn gdal_geotransform_to_bounds_pos_y_0() {
1✔
1957
        let gt = GdalDatasetGeoTransform {
1✔
1958
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1959
            x_pixel_size: 1.,
1✔
1960
            y_pixel_size: 1.,
1✔
1961
        };
1✔
1962

1✔
1963
        let sb = gt.spatial_partition(10, 10);
1✔
1964

1✔
1965
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
1966
            .unwrap();
1✔
1967

1✔
1968
        assert_eq!(sb, exp);
1✔
1969
    }
1✔
1970

1971
    #[test]
1✔
1972
    fn gdal_geotransform_to_bounds_pos_y_5() {
1✔
1973
        let gt = GdalDatasetGeoTransform {
1✔
1974
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
1975
            x_pixel_size: 0.5,
1✔
1976
            y_pixel_size: 0.5,
1✔
1977
        };
1✔
1978

1✔
1979
        let sb = gt.spatial_partition(10, 10);
1✔
1980

1✔
1981
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
1982
            .unwrap();
1✔
1983

1✔
1984
        assert_eq!(sb, exp);
1✔
1985
    }
1✔
1986

1987
    #[test]
1✔
1988
    fn gdal_read_window_data_origin_upper_left() {
1✔
1989
        let gt = GdalDatasetGeoTransform {
1✔
1990
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
1991
            x_pixel_size: 0.5,
1✔
1992
            y_pixel_size: -0.5,
1✔
1993
        };
1✔
1994

1✔
1995
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
1996
            .unwrap();
1✔
1997

1✔
1998
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
1999

1✔
2000
        let exp = GdalReadWindow {
1✔
2001
            read_size_x: 4,
1✔
2002
            read_size_y: 6,
1✔
2003
            read_start_x: 6,
1✔
2004
            read_start_y: 4,
1✔
2005
        };
1✔
2006

1✔
2007
        assert_eq!(rw, exp);
1✔
2008
    }
1✔
2009

2010
    #[test]
1✔
2011
    fn gdal_read_window_data_origin_lower_left() {
1✔
2012
        let gt = GdalDatasetGeoTransform {
1✔
2013
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2014
            x_pixel_size: 1.,
1✔
2015
            y_pixel_size: 1.,
1✔
2016
        };
1✔
2017

1✔
2018
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2019
            .unwrap();
1✔
2020

1✔
2021
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2022

1✔
2023
        let exp = GdalReadWindow {
1✔
2024
            read_size_x: 10,
1✔
2025
            read_size_y: 10,
1✔
2026
            read_start_x: 0,
1✔
2027
            read_start_y: 0,
1✔
2028
        };
1✔
2029

1✔
2030
        assert_eq!(rw, exp);
1✔
2031
    }
1✔
2032

2033
    #[test]
1✔
2034
    fn read_up_side_down_raster() {
1✔
2035
        let output_shape: GridShape2D = [8, 8].into();
1✔
2036
        let output_bounds =
1✔
2037
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2038

1✔
2039
        let up_side_down_params = GdalDatasetParameters {
1✔
2040
            file_path: test_data!(
1✔
2041
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2042
            )
1✔
2043
            .into(),
1✔
2044
            rasterband_channel: 1,
1✔
2045
            geo_transform: GdalDatasetGeoTransform {
1✔
2046
                origin_coordinate: (-180., -90.).into(),
1✔
2047
                x_pixel_size: 0.1,
1✔
2048
                y_pixel_size: 0.1,
1✔
2049
            },
1✔
2050
            width: 3600,
1✔
2051
            height: 1800,
1✔
2052
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2053
            no_data_value: Some(0.),
1✔
2054
            properties_mapping: Some(vec![
1✔
2055
                GdalMetadataMapping {
1✔
2056
                    source_key: RasterPropertiesKey {
1✔
2057
                        domain: None,
1✔
2058
                        key: "AREA_OR_POINT".to_string(),
1✔
2059
                    },
1✔
2060
                    target_type: RasterPropertiesEntryType::String,
1✔
2061
                    target_key: RasterPropertiesKey {
1✔
2062
                        domain: None,
1✔
2063
                        key: "AREA_OR_POINT".to_string(),
1✔
2064
                    },
1✔
2065
                },
1✔
2066
                GdalMetadataMapping {
1✔
2067
                    source_key: RasterPropertiesKey {
1✔
2068
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2069
                        key: "COMPRESSION".to_string(),
1✔
2070
                    },
1✔
2071
                    target_type: RasterPropertiesEntryType::String,
1✔
2072
                    target_key: RasterPropertiesKey {
1✔
2073
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2074
                        key: "COMPRESSION".to_string(),
1✔
2075
                    },
1✔
2076
                },
1✔
2077
            ]),
1✔
2078
            gdal_open_options: None,
1✔
2079
            gdal_config_options: None,
1✔
2080
            allow_alphaband_as_mask: true,
1✔
2081
            retry: None,
1✔
2082
        };
1✔
2083

1✔
2084
        let tile_information =
1✔
2085
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2086

1✔
2087
        let RasterTile2D {
1✔
2088
            global_geo_transform: _,
1✔
2089
            grid_array: grid,
1✔
2090
            tile_position: _,
1✔
2091
            time: _,
1✔
2092
            properties: _,
1✔
2093
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2094
            &up_side_down_params,
1✔
2095
            tile_information,
1✔
2096
            TimeInterval::default(),
1✔
2097
        )
1✔
2098
        .unwrap();
1✔
2099

1✔
2100
        assert!(!grid.is_empty());
1✔
2101

2102
        let grid = grid.into_materialized_masked_grid();
1✔
2103

1✔
2104
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2105
        assert_eq!(
1✔
2106
            grid.inner_grid.data,
1✔
2107
            &[
1✔
2108
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2109
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2110
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2111
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2112
            ]
1✔
2113
        );
1✔
2114

2115
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2116
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2117
    }
1✔
2118

2119
    #[test]
1✔
2120
    #[allow(clippy::too_many_lines)]
2121
    fn it_creates_no_data_only_for_missing_files() {
1✔
2122
        hide_gdal_errors();
1✔
2123

1✔
2124
        let ds = GdalDatasetParameters {
1✔
2125
            file_path: "nonexisting_file.tif".into(),
1✔
2126
            rasterband_channel: 1,
1✔
2127
            geo_transform: TestDefault::test_default(),
1✔
2128
            width: 100,
1✔
2129
            height: 100,
1✔
2130
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2131
            no_data_value: None,
1✔
2132
            properties_mapping: None,
1✔
2133
            gdal_open_options: None,
1✔
2134
            gdal_config_options: None,
1✔
2135
            allow_alphaband_as_mask: true,
1✔
2136
            retry: None,
1✔
2137
        };
1✔
2138

1✔
2139
        let tile_info = TileInformation {
1✔
2140
            tile_size_in_pixels: [100, 100].into(),
1✔
2141
            global_tile_position: [0, 0].into(),
1✔
2142
            global_geo_transform: TestDefault::test_default(),
1✔
2143
        };
1✔
2144

1✔
2145
        let tile_time = TimeInterval::default();
1✔
2146

1✔
2147
        // file doesn't exist => no data
1✔
2148
        let result = GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time).unwrap();
1✔
2149
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2150

2151
        let ds = GdalDatasetParameters {
1✔
2152
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2153
            rasterband_channel: 100, // invalid channel
1✔
2154
            geo_transform: TestDefault::test_default(),
1✔
2155
            width: 100,
1✔
2156
            height: 100,
1✔
2157
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2158
            no_data_value: None,
1✔
2159
            properties_mapping: None,
1✔
2160
            gdal_open_options: None,
1✔
2161
            gdal_config_options: None,
1✔
2162
            allow_alphaband_as_mask: true,
1✔
2163
            retry: None,
1✔
2164
        };
1✔
2165

1✔
2166
        // invalid channel => error
1✔
2167
        let result = GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time);
1✔
2168
        assert!(result.is_err());
1✔
2169

2170
        let server = Server::run();
1✔
2171

1✔
2172
        server.expect(
1✔
2173
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2174
                .times(1)
1✔
2175
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2176
        );
1✔
2177

1✔
2178
        server.expect(
1✔
2179
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2180
                .times(1)
1✔
2181
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2182
        );
1✔
2183

1✔
2184
        let ds = GdalDatasetParameters {
1✔
2185
            file_path: format!("/vsicurl/{}", server.url_str("/non_existing.tif")).into(),
1✔
2186
            rasterband_channel: 1,
1✔
2187
            geo_transform: TestDefault::test_default(),
1✔
2188
            width: 100,
1✔
2189
            height: 100,
1✔
2190
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2191
            no_data_value: None,
1✔
2192
            properties_mapping: None,
1✔
2193
            gdal_open_options: None,
1✔
2194
            gdal_config_options: Some(vec![
1✔
2195
                (
1✔
2196
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2197
                    ".tif".to_owned(),
1✔
2198
                ),
1✔
2199
                (
1✔
2200
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2201
                    "EMPTY_DIR".to_owned(),
1✔
2202
                ),
1✔
2203
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2204
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2205
            ]),
1✔
2206
            allow_alphaband_as_mask: true,
1✔
2207
            retry: None,
1✔
2208
        };
1✔
2209

1✔
2210
        // 404 => no data
1✔
2211
        let result = GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time).unwrap();
1✔
2212
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2213

2214
        let ds = GdalDatasetParameters {
1✔
2215
            file_path: format!("/vsicurl/{}", server.url_str("/internal_error.tif")).into(),
1✔
2216
            rasterband_channel: 1,
1✔
2217
            geo_transform: TestDefault::test_default(),
1✔
2218
            width: 100,
1✔
2219
            height: 100,
1✔
2220
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2221
            no_data_value: None,
1✔
2222
            properties_mapping: None,
1✔
2223
            gdal_open_options: None,
1✔
2224
            gdal_config_options: Some(vec![
1✔
2225
                (
1✔
2226
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2227
                    ".tif".to_owned(),
1✔
2228
                ),
1✔
2229
                (
1✔
2230
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2231
                    "EMPTY_DIR".to_owned(),
1✔
2232
                ),
1✔
2233
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2234
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2235
            ]),
1✔
2236
            allow_alphaband_as_mask: true,
1✔
2237
            retry: None,
1✔
2238
        };
1✔
2239

1✔
2240
        // 500 => error
1✔
2241
        let result = GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time);
1✔
2242
        assert!(result.is_err());
1✔
2243
    }
1✔
2244

2245
    #[test]
1✔
2246
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2247
        hide_gdal_errors();
1✔
2248

1✔
2249
        let server = Server::run();
1✔
2250

1✔
2251
        server.expect(
1✔
2252
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2253
                .times(2)
1✔
2254
                .respond_with(responders::cycle![
1✔
2255
                    // first generic error
1✔
2256
                    responders::status_code(500),
1✔
2257
                    // then 404 file not found
1✔
2258
                    responders::status_code(404)
1✔
2259
                ]),
1✔
2260
        );
1✔
2261

1✔
2262
        let file_path: PathBuf = format!("/vsicurl/{}", server.url_str("/foo.tif")).into();
1✔
2263

1✔
2264
        let options = Some(vec![
1✔
2265
            (
1✔
2266
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2267
                ".tif".to_owned(),
1✔
2268
            ),
1✔
2269
            (
1✔
2270
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2271
                "EMPTY_DIR".to_owned(),
1✔
2272
            ),
1✔
2273
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2274
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2275
        ]);
1✔
2276

1✔
2277
        let _thread_local_configs = options
1✔
2278
            .as_ref()
1✔
2279
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2280

1✔
2281
        // first fail
1✔
2282
        let result = gdal_open_dataset_ex(
1✔
2283
            file_path.as_path(),
1✔
2284
            DatasetOptions {
1✔
2285
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2286
                ..DatasetOptions::default()
1✔
2287
            },
1✔
2288
        );
1✔
2289

1✔
2290
        // it failed, but not with file not found
1✔
2291
        assert!(result.is_err());
1✔
2292
        if let Err(error) = result {
1✔
2293
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2294
        }
×
2295

2296
        // second fail doesn't even try, so still not "file not found", even though it should be now
2297
        let result = gdal_open_dataset_ex(
1✔
2298
            file_path.as_path(),
1✔
2299
            DatasetOptions {
1✔
2300
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2301
                ..DatasetOptions::default()
1✔
2302
            },
1✔
2303
        );
1✔
2304

1✔
2305
        assert!(result.is_err());
1✔
2306
        if let Err(error) = result {
1✔
2307
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2308
        }
×
2309

2310
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2311

1✔
2312
        // after clearing the cache, it tries again
1✔
2313
        let result = gdal_open_dataset_ex(
1✔
2314
            file_path.as_path(),
1✔
2315
            DatasetOptions {
1✔
2316
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2317
                ..DatasetOptions::default()
1✔
2318
            },
1✔
2319
        );
1✔
2320

1✔
2321
        // now we get the file not found error
1✔
2322
        assert!(result.is_err());
1✔
2323
        if let Err(error) = result {
1✔
2324
            assert!(error_is_gdal_file_not_found(&error));
1✔
2325
        }
×
2326
    }
1✔
2327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc