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

geo-engine / geoengine / 4074665624

pending completion
4074665624

push

github

GitHub
Merge #732

415 of 415 new or added lines in 16 files covered. (100.0%)

88107 of 100193 relevant lines covered (87.94%)

77996.79 hits per line

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

93.64
/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)]
67✔
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)]
203✔
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)]
203✔
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,501✔
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) {
533✔
165
        (self.read_start_x, self.read_start_y)
533✔
166
    }
533✔
167

168
    fn gdal_window_size(&self) -> (usize, usize) {
533✔
169
        (self.read_size_x, self.read_size_y)
533✔
170
    }
533✔
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,501✔
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,102✔
189
        // the opposite y value (y value of the non origin edge)
1,102✔
190
        let opposite_coord_y = self.origin_coordinate.y + self.y_pixel_size * y_size as f64;
1,102✔
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,102✔
194
            (self.origin_coordinate.y, opposite_coord_y)
1,099✔
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,102✔
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,102✔
203
            (self.origin_coordinate.x, opposite_coord_x)
1,102✔
204
        } else {
205
            (opposite_coord_x, self.origin_coordinate.x)
×
206
        };
207

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

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

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

226
    fn spatial_partition_to_read_window(
527✔
227
        &self,
527✔
228
        spatial_partition: &SpatialPartition2D,
527✔
229
    ) -> GdalReadWindow {
527✔
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`].
527✔
231
        const EPSILON: f64 = 0.000_001;
527✔
232
        let epsilon: Coordinate2D =
527✔
233
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
527✔
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() {
527✔
266
            (
525✔
267
                spatial_partition.upper_left(),
525✔
268
                spatial_partition.lower_right(),
525✔
269
            )
525✔
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;
527✔
279
        // Move the coordinate far from the origin a bit inside the bbox by subtracting an epsilon of the pixel size
527✔
280
        let safe_far_coord = far_origin_coord - epsilon;
527✔
281

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

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

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

527✔
291
        GdalReadWindow {
527✔
292
            read_start_x: near_idx_x,
527✔
293
            read_start_y: near_idx_y,
527✔
294
            read_size_x,
527✔
295
            read_size_y,
527✔
296
        }
527✔
297
    }
527✔
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
525✔
315
    where
525✔
316
        M: Into<Self::Margin>,
525✔
317
    {
525✔
318
        let m = margin.into();
525✔
319
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
525✔
320
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
525✔
321
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
525✔
322
    }
525✔
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 {
580✔
345
        Self {
580✔
346
            origin_coordinate: (gdal_geo_transform[0], gdal_geo_transform[3]).into(),
580✔
347
            x_pixel_size: gdal_geo_transform[1],
580✔
348
            y_pixel_size: gdal_geo_transform[5],
580✔
349
        }
580✔
350
    }
580✔
351
}
352

353
impl SpatialPartitioned for GdalDatasetParameters {
354
    fn spatial_partition(&self) -> SpatialPartition2D {
573✔
355
        self.geo_transform
573✔
356
            .spatial_partition(self.width, self.height)
573✔
357
    }
573✔
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,501✔
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(
107✔
379
        &self,
107✔
380
        placeholders: &HashMap<String, GdalSourceTimePlaceholder>,
107✔
381
        time: TimeInterval,
107✔
382
    ) -> Result<Self> {
107✔
383
        let mut file_path: String = self.file_path.to_string_lossy().into();
107✔
384

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

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

399
        Ok(Self {
107✔
400
            file_not_found_handling: self.file_not_found_handling,
107✔
401
            file_path: file_path.into(),
107✔
402
            properties_mapping: self.properties_mapping.clone(),
107✔
403
            gdal_open_options: self.gdal_open_options.clone(),
107✔
404
            gdal_config_options: self.gdal_config_options.clone(),
107✔
405
            ..*self
107✔
406
        })
107✔
407
    }
107✔
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>(
519✔
434
        dataset_params: GdalDatasetParameters,
519✔
435
        tile_information: TileInformation,
519✔
436
        tile_time: TimeInterval,
519✔
437
    ) -> Result<RasterTile2D<T>> {
519✔
438
        // TODO: detect usage of vsi curl properly, e.g. also check for `/vsicurl_streaming` and combinations with `/vsizip`
519✔
439
        let is_vsi_curl = dataset_params.file_path.starts_with("/vsicurl/");
519✔
440

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

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

460
                    match load_tile_result {
509✔
461
                        Ok(Ok(r)) => Ok(r),
503✔
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
                }
509✔
473
            },
525✔
474
        )
519✔
475
        .await
504✔
476
    }
503✔
477

478
    async fn load_tile_async<T: Pixel + GdalType + FromPrimitive>(
637✔
479
        dataset_params: Option<GdalDatasetParameters>,
637✔
480
        tile_information: TileInformation,
637✔
481
        tile_time: TimeInterval,
637✔
482
    ) -> Result<RasterTile2D<T>> {
637✔
483
        match dataset_params {
573✔
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)
519✔
486
                if tile_information
573✔
487
                    .spatial_partition()
573✔
488
                    .intersects(&ds.spatial_partition()) =>
573✔
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
519✔
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
    }
602✔
512

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

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

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

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

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

549
        if let Err(error) = &dataset_result {
533✔
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
                "file not found -> returning error = {}, took: {:?}, file: {:?}",
×
563
                err_result.is_err(),
×
564
                elapsed,
565
                dataset_params.file_path
566
            );
567
            return err_result;
7✔
568
        };
526✔
569

526✔
570
        let dataset = dataset_result.expect("checked");
526✔
571

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

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

582
        Ok(result_tile)
523✔
583
    }
533✔
584

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

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

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

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

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

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

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

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

664
        let spatial_resolution = query.spatial_resolution;
106✔
665

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

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

683
        let result_descriptor = self.meta_data.result_descriptor().await?;
106✔
684

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

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

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

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

715
        let source_stream = stream::iter(loading_iter);
106✔
716

106✔
717
        let source_stream =
106✔
718
            GdalRasterLoader::loading_info_to_tile_stream(source_stream, query, tiling_strategy);
106✔
719

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

731
pub type GdalSource = SourceOperator<GdalSourceParameters>;
732

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

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

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

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

42✔
754
        Ok(op.boxed())
42✔
755
    }
84✔
756

757
    span_fn!(GdalSource);
1✔
758
}
759

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

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

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

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

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

872
    let data_grid = if flip_y_axis {
523✔
873
        data_grid.reversed_y_axis_grid()
1✔
874
    } else {
875
        data_grid
522✔
876
    };
877

878
    let dataset_mask_flags = rasterband.mask_flags()?;
523✔
879

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

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

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

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

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

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

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

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

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

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

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

979
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
525✔
980
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
525✔
981

982
    let gdal_dataset_bounds =
525✔
983
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
525✔
984

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

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

993
    let tile_geo_transform = tile_info.tile_geo_transform();
525✔
994

525✔
995
    let gdal_read_window =
525✔
996
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
525✔
997

525✔
998
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
525✔
999
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
525✔
1000

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

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

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

1027
    Ok(result_grid)
523✔
1028
}
525✔
1029

1030
/// 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.
1031
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
526✔
1032
    dataset: &GdalDataset,
526✔
1033
    dataset_params: &GdalDatasetParameters,
526✔
1034
    tile_info: TileInformation,
526✔
1035
    tile_time: TimeInterval,
526✔
1036
) -> Result<RasterTile2D<T>> {
526✔
1037
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
526✔
1038

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

1041
    let mut properties = RasterProperties::default();
523✔
1042

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

1050
    Ok(RasterTile2D::new_with_tile_info_and_properties(
523✔
1051
        tile_time,
523✔
1052
        tile_info,
523✔
1053
        result_grid,
523✔
1054
        properties,
523✔
1055
    ))
523✔
1056
}
526✔
1057

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
2320
        // now we get the file not found error
1✔
2321
        assert!(result.is_err());
1✔
2322
        if let Err(error) = result {
1✔
2323
            assert!(error_is_gdal_file_not_found(&error));
1✔
2324
        }
×
2325
    }
1✔
2326
}
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