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

geo-engine / geoengine / 5751943360

03 Aug 2023 02:19PM UTC coverage: 89.422% (+0.4%) from 88.974%
5751943360

push

github

web-flow
Merge pull request #840 from geo-engine/remove_in_memory

Remove in memory contexts and dbs

5338 of 5338 new or added lines in 37 files covered. (100.0%)

103772 of 116048 relevant lines covered (89.42%)

62390.21 hits per line

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

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

62
mod error;
63
mod loading_info;
64

65
static GDAL_RETRY_INITIAL_BACKOFF_MS: u64 = 1000;
66
static GDAL_RETRY_MAX_BACKOFF_MS: u64 = 60 * 60 * 1000;
67
static GDAL_RETRY_EXPONENTIAL_BACKOFF_FACTOR: f64 = 2.;
68

69
/// Parameters for the GDAL Source Operator
70
///
71
/// # Examples
72
///
73
/// ```rust
74
/// use serde_json::{Result, Value};
75
/// use geoengine_operators::source::{GdalSource, GdalSourceParameters};
76
/// use geoengine_datatypes::dataset::{NamedData};
77
/// use geoengine_datatypes::util::Identifier;
78
///
79
/// let json_string = r#"
80
///     {
81
///         "type": "GdalSource",
82
///         "params": {
83
///             "data": "ns:dataset"
84
///         }
85
///     }"#;
86
///
87
/// let operator: GdalSource = serde_json::from_str(json_string).unwrap();
88
///
89
/// assert_eq!(operator, GdalSource {
90
///     params: GdalSourceParameters {
91
///         data: NamedData::with_namespaced_name("ns", "dataset"),
92
///     },
93
/// });
94
/// ```
95
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
176✔
96
pub struct GdalSourceParameters {
97
    pub data: NamedData,
98
}
99

100
impl OperatorData for GdalSourceParameters {
101
    fn data_names_collect(&self, data_names: &mut Vec<NamedData>) {
2✔
102
        data_names.push(self.data.clone());
2✔
103
    }
2✔
104
}
105

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

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

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

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

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

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

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

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

172
/// A user friendly representation of Gdal's geo transform. In contrast to [`GeoTransform`] this
173
/// geo transform allows arbitrary pixel sizes and can thus also represent rasters where the origin is not located
174
/// in the upper left corner. It should only be used for loading rasters with Gdal and not internally.
175
/// 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.
176
/// However, there are datasets where the data is stored "upside-down". If this is the case, the pixel size is positive.
177
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
1,611✔
178
#[serde(rename_all = "camelCase")]
179
pub struct GdalDatasetGeoTransform {
180
    pub origin_coordinate: Coordinate2D,
181
    pub x_pixel_size: f64,
182
    pub y_pixel_size: f64,
183
}
184

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

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

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

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

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

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

1,256✔
222
        [grid_y_index, grid_x_index].into()
1,256✔
223
    }
1,256✔
224

225
    fn spatial_partition_to_read_window(
628✔
226
        &self,
628✔
227
        spatial_partition: &SpatialPartition2D,
628✔
228
    ) -> GdalReadWindow {
628✔
229
        // 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`].
628✔
230
        const EPSILON: f64 = 0.000_001;
628✔
231
        let epsilon: Coordinate2D =
628✔
232
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
628✔
233

234
        /*
235
        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:
236

237
        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.
238
        ul                      ur
239
        +_______________________+
240
        |_|_ row 1              |
241
        | |_|_  row 2           |
242
        |   |_|_  row ...       |
243
        |     |_|               |
244
        |_______________________|
245
        +                       *
246
        ll                      lr
247

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

250
        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.
251

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

262
        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.
263
        */
264
        let (near_origin_coord, far_origin_coord) = if self.y_pixel_size.is_sign_negative() {
628✔
265
            (
626✔
266
                spatial_partition.upper_left(),
626✔
267
                spatial_partition.lower_right(),
626✔
268
            )
626✔
269
        } else {
270
            (
2✔
271
                spatial_partition.lower_left(),
2✔
272
                spatial_partition.upper_right(),
2✔
273
            )
2✔
274
        };
275

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

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

284
        debug_assert!(near_idx_x <= far_idx_x);
628✔
285
        debug_assert!(near_idx_y <= far_idx_y);
628✔
286

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

426
struct GdalRasterLoader {}
427

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

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

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

460
                    match load_tile_result {
607✔
461
                        Ok(Ok(r)) => Ok(r),
601✔
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
                }
607✔
473
            },
625✔
474
        )
619✔
475
        .await
610✔
476
    }
601✔
477

478
    async fn load_tile_async<T: Pixel + GdalType + FromPrimitive>(
713✔
479
        dataset_params: Option<GdalDatasetParameters>,
713✔
480
        tile_information: TileInformation,
713✔
481
        tile_time: TimeInterval,
713✔
482
        cache_hint: CacheHint,
713✔
483
    ) -> Result<RasterTile2D<T>> {
713✔
484
        match dataset_params {
652✔
485
            // 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.
486
            Some(ds)
619✔
487
                if tile_information
652✔
488
                    .spatial_partition()
652✔
489
                    .intersects(&ds.spatial_partition()) =>
652✔
490
            {
491
                debug!(
492
                    "Loading tile {:?}, from {:?}, band: {}",
×
493
                    &tile_information, ds.file_path, ds.rasterband_channel
×
494
                );
495
                Self::load_tile_data_async(ds, tile_information, tile_time, cache_hint).await
619✔
496
            }
497
            Some(_) => {
498
                debug!("Skipping tile not in query rect {:?}", &tile_information);
×
499

500
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
33✔
501
            }
502

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

509
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
46✔
510
            }
511
        }
512
    }
680✔
513

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

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

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

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

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

551
        if let Err(error) = &dataset_result {
634✔
552
            let is_file_not_found = error_is_gdal_file_not_found(error);
7✔
553

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

627✔
573
        let dataset = dataset_result.expect("checked");
627✔
574

575
        let result_tile = read_raster_tile_with_properties(
627✔
576
            &dataset,
627✔
577
            dataset_params,
627✔
578
            tile_information,
627✔
579
            tile_time,
627✔
580
            cache_hint,
627✔
581
        )?;
627✔
582

583
        let elapsed = start.elapsed();
624✔
584
        debug!("data loaded -> returning data grid, took {:?}", elapsed);
624✔
585

586
        Ok(result_tile)
624✔
587
    }
634✔
588

589
    ///
590
    /// A stream of futures producing `RasterTile2D` for a single slice in time
591
    ///
592
    fn temporal_slice_tile_future_stream<T: Pixel + GdalType + FromPrimitive>(
121✔
593
        query: RasterQueryRectangle,
121✔
594
        info: GdalLoadingInfoTemporalSlice,
121✔
595
        tiling_strategy: TilingStrategy,
121✔
596
    ) -> impl Stream<Item = impl Future<Output = Result<RasterTile2D<T>>>> {
121✔
597
        stream::iter(tiling_strategy.tile_information_iterator(query.spatial_bounds)).map(
121✔
598
            move |tile| {
711✔
599
                GdalRasterLoader::load_tile_async(
711✔
600
                    info.params.clone(),
711✔
601
                    tile,
711✔
602
                    info.time,
711✔
603
                    info.cache_ttl.into(),
711✔
604
                )
711✔
605
            },
711✔
606
        )
121✔
607
    }
121✔
608

609
    fn loading_info_to_tile_stream<
109✔
610
        T: Pixel + GdalType + FromPrimitive,
109✔
611
        S: Stream<Item = Result<GdalLoadingInfoTemporalSlice>>,
109✔
612
    >(
109✔
613
        loading_info_stream: S,
109✔
614
        query: RasterQueryRectangle,
109✔
615
        tiling_strategy: TilingStrategy,
109✔
616
    ) -> impl Stream<Item = Result<RasterTile2D<T>>> {
109✔
617
        loading_info_stream
109✔
618
            .map_ok(move |info| {
121✔
619
                GdalRasterLoader::temporal_slice_tile_future_stream(query, info, tiling_strategy)
121✔
620
                    .map(Result::Ok)
121✔
621
            })
121✔
622
            .try_flatten()
109✔
623
            .try_buffered(16) // TODO: make this configurable
109✔
624
    }
109✔
625
}
626

627
fn error_is_gdal_file_not_found(error: &Error) -> bool {
10✔
628
    matches!(
3✔
629
        error,
10✔
630
        Error::Gdal {
10✔
631
            source: GdalError::NullPointer {
10✔
632
                method_name,
10✔
633
                msg
10✔
634
            },
10✔
635
        } if *method_name == "GDALOpenEx" && (*msg == "HTTP response code: 404" || msg.ends_with("No such file or directory"))
10✔
636
    )
637
}
10✔
638

639
fn clear_gdal_vsi_cache_for_path(file_path: &Path) {
640
    unsafe {
641
        if let Some(Some(c_string)) = file_path.to_str().map(|s| CString::new(s).ok()) {
7✔
642
            VSICurlPartialClearCache(c_string.as_ptr());
7✔
643
        }
7✔
644
    }
645
}
7✔
646

647
impl<T> GdalSourceProcessor<T> where T: gdal::raster::GdalType + Pixel {}
648

649
#[async_trait]
650
impl<P> QueryProcessor for GdalSourceProcessor<P>
651
where
652
    P: Pixel + gdal::raster::GdalType + FromPrimitive,
653
{
654
    type Output = RasterTile2D<P>;
655
    type SpatialBounds = SpatialPartition2D;
656

657
    async fn _query<'a>(
109✔
658
        &'a self,
109✔
659
        query: RasterQueryRectangle,
109✔
660
        _ctx: &'a dyn crate::engine::QueryContext,
109✔
661
    ) -> Result<BoxStream<Result<Self::Output>>> {
109✔
662
        let start = Instant::now();
109✔
663
        debug!(
109✔
664
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
×
665
            P::TYPE,
×
666
            &query
×
667
        );
668

669
        debug!(
109✔
670
            "GdalSourceProcessor<{:?}> meta data loaded, took {:?}.",
×
671
            P::TYPE,
×
672
            start.elapsed()
×
673
        );
674

675
        let spatial_resolution = query.spatial_resolution;
109✔
676

109✔
677
        // A `GeoTransform` maps pixel space to world space.
109✔
678
        // Usually a SRS has axis directions pointing "up" (y-axis) and "up" (y-axis).
109✔
679
        // We are not aware of spatial reference systems where the x-axis points to the right.
109✔
680
        // However, there are spatial reference systems where the y-axis points downwards.
109✔
681
        // The standard "pixel-space" starts at the top-left corner of a `GeoTransform` and points down-right.
109✔
682
        // Therefore, the pixel size on the x-axis is always increasing
109✔
683
        let pixel_size_x = spatial_resolution.x;
109✔
684
        debug_assert!(pixel_size_x.is_sign_positive());
109✔
685
        // and the y-axis should only be positive if the y-axis of the spatial reference system also "points down".
686
        // NOTE: at the moment we do not allow "down pointing" y-axis.
687
        let pixel_size_y = spatial_resolution.y * -1.0;
109✔
688
        debug_assert!(pixel_size_y.is_sign_negative());
109✔
689

690
        let tiling_strategy = self
109✔
691
            .tiling_specification
109✔
692
            .strategy(pixel_size_x, pixel_size_y);
109✔
693

694
        let result_descriptor = self.meta_data.result_descriptor().await?;
109✔
695

696
        let mut empty = false;
109✔
697
        debug!("result descr bbox: {:?}", result_descriptor.bbox);
109✔
698
        debug!("query bbox: {:?}", query.spatial_bounds);
109✔
699

700
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
109✔
701
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
70✔
702
                debug!("query does not intersect spatial data bounds");
2✔
703
                empty = true;
2✔
704
            }
68✔
705
        }
39✔
706

707
        // TODO: use the time bounds to early return.
708
        /*
709
        if let Some(data_time_bounds) = result_descriptor.time {
710
            if !data_time_bounds.intersects(&query.time_interval) {
711
                debug!("query does not intersect temporal data bounds");
712
                empty = true;
713
            }
714
        }
715
        */
716

717
        let loading_iter = if empty {
109✔
718
            GdalLoadingInfoTemporalSliceIterator::Static {
2✔
719
                parts: vec![].into_iter(),
2✔
720
            }
2✔
721
        } else {
722
            let loading_info = self.meta_data.loading_info(query).await?;
107✔
723
            loading_info.info
107✔
724
        };
725

726
        let source_stream = stream::iter(loading_iter);
109✔
727

109✔
728
        let source_stream =
109✔
729
            GdalRasterLoader::loading_info_to_tile_stream(source_stream, query, tiling_strategy);
109✔
730

109✔
731
        // use SparseTilesFillAdapter to fill all the gaps
109✔
732
        let filled_stream = SparseTilesFillAdapter::new(
109✔
733
            source_stream,
109✔
734
            tiling_strategy.tile_grid_box(query.spatial_partition()),
109✔
735
            tiling_strategy.geo_transform,
109✔
736
            tiling_strategy.tile_size_in_pixels,
109✔
737
            FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles,
109✔
738
        );
109✔
739
        Ok(filled_stream.boxed())
109✔
740
    }
218✔
741
}
742

743
pub type GdalSource = SourceOperator<GdalSourceParameters>;
744

745
impl OperatorName for GdalSource {
746
    const TYPE_NAME: &'static str = "GdalSource";
747
}
748

749
#[typetag::serde]
30✔
750
#[async_trait]
751
impl RasterOperator for GdalSource {
752
    async fn _initialize(
49✔
753
        self: Box<Self>,
49✔
754
        path: WorkflowOperatorPath,
49✔
755
        context: &dyn crate::engine::ExecutionContext,
49✔
756
    ) -> Result<Box<dyn InitializedRasterOperator>> {
49✔
757
        let data_id = context.resolve_named_data(&self.params.data).await?;
59✔
758
        let meta_data: GdalMetaData = context.meta_data(&data_id).await?;
69✔
759

760
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
48✔
761
        debug!("GdalSource path: {:?}", path);
48✔
762

763
        let op = InitializedGdalSourceOperator {
48✔
764
            name: CanonicOperatorName::from(&self),
48✔
765
            result_descriptor: meta_data.result_descriptor().await?,
48✔
766
            meta_data,
48✔
767
            tiling_specification: context.tiling_specification(),
48✔
768
        };
48✔
769

48✔
770
        Ok(op.boxed())
48✔
771
    }
98✔
772

773
    span_fn!(GdalSource);
1✔
774
}
775

776
pub struct InitializedGdalSourceOperator {
777
    name: CanonicOperatorName,
778
    pub meta_data: GdalMetaData,
779
    pub result_descriptor: RasterResultDescriptor,
780
    pub tiling_specification: TilingSpecification,
781
}
782

783
impl InitializedRasterOperator for InitializedGdalSourceOperator {
784
    fn result_descriptor(&self) -> &RasterResultDescriptor {
83✔
785
        &self.result_descriptor
83✔
786
    }
83✔
787

788
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
41✔
789
        Ok(match self.result_descriptor().data_type {
41✔
790
            RasterDataType::U8 => TypedRasterQueryProcessor::U8(
38✔
791
                GdalSourceProcessor {
38✔
792
                    tiling_specification: self.tiling_specification,
38✔
793
                    meta_data: self.meta_data.clone(),
38✔
794
                    _phantom_data: PhantomData,
38✔
795
                }
38✔
796
                .boxed(),
38✔
797
            ),
38✔
798
            RasterDataType::U16 => TypedRasterQueryProcessor::U16(
2✔
799
                GdalSourceProcessor {
2✔
800
                    tiling_specification: self.tiling_specification,
2✔
801
                    meta_data: self.meta_data.clone(),
2✔
802
                    _phantom_data: PhantomData,
2✔
803
                }
2✔
804
                .boxed(),
2✔
805
            ),
2✔
806
            RasterDataType::U32 => TypedRasterQueryProcessor::U32(
×
807
                GdalSourceProcessor {
×
808
                    tiling_specification: self.tiling_specification,
×
809
                    meta_data: self.meta_data.clone(),
×
810
                    _phantom_data: PhantomData,
×
811
                }
×
812
                .boxed(),
×
813
            ),
×
814
            RasterDataType::U64 => {
815
                return Err(GdalSourceError::UnsupportedRasterType {
×
816
                    raster_type: RasterDataType::U64,
×
817
                })?
×
818
            }
819
            RasterDataType::I8 => {
820
                return Err(GdalSourceError::UnsupportedRasterType {
×
821
                    raster_type: RasterDataType::I8,
×
822
                })?
×
823
            }
824
            RasterDataType::I16 => TypedRasterQueryProcessor::I16(
1✔
825
                GdalSourceProcessor {
1✔
826
                    tiling_specification: self.tiling_specification,
1✔
827
                    meta_data: self.meta_data.clone(),
1✔
828
                    _phantom_data: PhantomData,
1✔
829
                }
1✔
830
                .boxed(),
1✔
831
            ),
1✔
832
            RasterDataType::I32 => TypedRasterQueryProcessor::I32(
×
833
                GdalSourceProcessor {
×
834
                    tiling_specification: self.tiling_specification,
×
835
                    meta_data: self.meta_data.clone(),
×
836
                    _phantom_data: PhantomData,
×
837
                }
×
838
                .boxed(),
×
839
            ),
×
840
            RasterDataType::I64 => {
841
                return Err(GdalSourceError::UnsupportedRasterType {
×
842
                    raster_type: RasterDataType::I64,
×
843
                })?
×
844
            }
845
            RasterDataType::F32 => TypedRasterQueryProcessor::F32(
×
846
                GdalSourceProcessor {
×
847
                    tiling_specification: self.tiling_specification,
×
848
                    meta_data: self.meta_data.clone(),
×
849
                    _phantom_data: PhantomData,
×
850
                }
×
851
                .boxed(),
×
852
            ),
×
853
            RasterDataType::F64 => TypedRasterQueryProcessor::F64(
×
854
                GdalSourceProcessor {
×
855
                    tiling_specification: self.tiling_specification,
×
856
                    meta_data: self.meta_data.clone(),
×
857
                    _phantom_data: PhantomData,
×
858
                }
×
859
                .boxed(),
×
860
            ),
×
861
        })
862
    }
41✔
863

864
    fn canonic_name(&self) -> CanonicOperatorName {
1✔
865
        self.name.clone()
1✔
866
    }
1✔
867
}
868

869
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
870
/// It fails if the tile is not within the dataset.
871
#[allow(clippy::float_cmp)]
872
fn read_grid_from_raster<T, D>(
626✔
873
    rasterband: &GdalRasterBand,
626✔
874
    read_window: &GdalReadWindow,
626✔
875
    out_shape: D,
626✔
876
    dataset_params: &GdalDatasetParameters,
626✔
877
    flip_y_axis: bool,
626✔
878
) -> Result<GridOrEmpty<D, T>>
626✔
879
where
626✔
880
    T: Pixel + GdalType + Default + FromPrimitive,
626✔
881
    D: Clone + GridSize + PartialEq,
626✔
882
{
626✔
883
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
626✔
884

885
    let buffer = rasterband.read_as::<T>(
626✔
886
        read_window.gdal_window_start(), // pixelspace origin
626✔
887
        read_window.gdal_window_size(),  // pixelspace size
626✔
888
        gdal_out_shape,                  // requested raster size
626✔
889
        None,                            // sampling mode
626✔
890
    )?;
626✔
891
    let data_grid = Grid::new(out_shape.clone(), buffer.data)?;
624✔
892

893
    let data_grid = if flip_y_axis {
624✔
894
        data_grid.reversed_y_axis_grid()
1✔
895
    } else {
896
        data_grid
623✔
897
    };
898

899
    let dataset_mask_flags = rasterband.mask_flags()?;
624✔
900

901
    if dataset_mask_flags.is_all_valid() {
624✔
902
        debug!("all pixels are valid --> skip no-data and mask handling.");
×
903
        return Ok(MaskedGrid::new_with_data(data_grid).into());
×
904
    }
624✔
905

624✔
906
    if dataset_mask_flags.is_nodata() {
624✔
907
        debug!("raster uses a no-data value --> use no-data handling.");
616✔
908
        let no_data_value = dataset_params
616✔
909
            .no_data_value
616✔
910
            .or_else(|| rasterband.no_data_value())
616✔
911
            .and_then(FromPrimitive::from_f64);
616✔
912
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
616✔
913
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
616✔
914
        return Ok(grid_or_empty);
616✔
915
    }
8✔
916

8✔
917
    if dataset_mask_flags.is_alpha() {
8✔
918
        debug!("raster uses alpha band to mask pixels.");
×
919
        if !dataset_params.allow_alphaband_as_mask {
×
920
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
921
        }
×
922
    }
8✔
923

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

926
    let mask_band = rasterband.open_mask_band()?;
8✔
927
    let mask_buffer = mask_band.read_as::<u8>(
8✔
928
        read_window.gdal_window_start(), // pixelspace origin
8✔
929
        read_window.gdal_window_size(),  // pixelspace size
8✔
930
        gdal_out_shape,                  // requested raster size
8✔
931
        None,                            // sampling mode
8✔
932
    )?;
8✔
933
    let mask_grid = Grid::new(out_shape, mask_buffer.data)?.map_elements(|p: u8| p > 0);
360,016✔
934

935
    let mask_grid = if flip_y_axis {
8✔
936
        mask_grid.reversed_y_axis_grid()
×
937
    } else {
938
        mask_grid
8✔
939
    };
940

941
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
8✔
942
    Ok(GridOrEmpty::from(masked_grid))
8✔
943
}
626✔
944

945
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
946
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
947
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
948
fn read_partial_grid_from_raster<T>(
349✔
949
    rasterband: &GdalRasterBand,
349✔
950
    gdal_read_window: &GdalReadWindow,
349✔
951
    out_tile_read_bounds: GridBoundingBox2D,
349✔
952
    out_tile_shape: GridShape2D,
349✔
953
    dataset_params: &GdalDatasetParameters,
349✔
954
    flip_y_axis: bool,
349✔
955
) -> Result<GridOrEmpty2D<T>>
349✔
956
where
349✔
957
    T: Pixel + GdalType + Default + FromPrimitive,
349✔
958
{
349✔
959
    let dataset_raster = read_grid_from_raster(
349✔
960
        rasterband,
349✔
961
        gdal_read_window,
349✔
962
        out_tile_read_bounds,
349✔
963
        dataset_params,
349✔
964
        flip_y_axis,
349✔
965
    )?;
349✔
966

967
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
347✔
968
    tile_raster.grid_blit_from(&dataset_raster);
347✔
969
    Ok(tile_raster)
347✔
970
}
349✔
971

972
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
973
/// It handles conversion to grid coordinates.
974
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
975
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
976
fn read_grid_and_handle_edges<T>(
626✔
977
    tile_info: TileInformation,
626✔
978
    dataset: &GdalDataset,
626✔
979
    rasterband: &GdalRasterBand,
626✔
980
    dataset_params: &GdalDatasetParameters,
626✔
981
) -> Result<GridOrEmpty2D<T>>
626✔
982
where
626✔
983
    T: Pixel + GdalType + Default + FromPrimitive,
626✔
984
{
626✔
985
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
626✔
986
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
626✔
987

626✔
988
    if !approx_eq!(
626✔
989
        GdalDatasetGeoTransform,
626✔
990
        gdal_dataset_geotransform,
626✔
991
        dataset_params.geo_transform
626✔
992
    ) {
626✔
993
        log::warn!(
1✔
994
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
995
            dataset_params.geo_transform,
996
            gdal_dataset_geotransform,
997
        );
998
    };
625✔
999

1000
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
626✔
1001
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
626✔
1002

1003
    let gdal_dataset_bounds =
626✔
1004
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
626✔
1005

626✔
1006
    let output_bounds = tile_info.spatial_partition();
626✔
1007
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
626✔
1008
    let output_shape = tile_info.tile_size_in_pixels();
626✔
1009

1010
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
626✔
1011
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
1012
    };
1013

1014
    let tile_geo_transform = tile_info.tile_geo_transform();
626✔
1015

626✔
1016
    let gdal_read_window =
626✔
1017
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
626✔
1018

626✔
1019
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
626✔
1020
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
626✔
1021

626✔
1022
    if is_y_axis_flipped {
626✔
1023
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1024
    }
625✔
1025

1026
    let result_grid = if dataset_intersection_area == output_bounds {
626✔
1027
        read_grid_from_raster(
277✔
1028
            rasterband,
277✔
1029
            &gdal_read_window,
277✔
1030
            output_shape,
277✔
1031
            dataset_params,
277✔
1032
            is_y_axis_flipped,
277✔
1033
        )?
277✔
1034
    } else {
1035
        let partial_tile_grid_bounds =
349✔
1036
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
349✔
1037

349✔
1038
        read_partial_grid_from_raster(
349✔
1039
            rasterband,
349✔
1040
            &gdal_read_window,
349✔
1041
            partial_tile_grid_bounds,
349✔
1042
            output_shape,
349✔
1043
            dataset_params,
349✔
1044
            is_y_axis_flipped,
349✔
1045
        )?
349✔
1046
    };
1047

1048
    Ok(result_grid)
624✔
1049
}
626✔
1050

1051
/// 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.
1052
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
627✔
1053
    dataset: &GdalDataset,
627✔
1054
    dataset_params: &GdalDatasetParameters,
627✔
1055
    tile_info: TileInformation,
627✔
1056
    tile_time: TimeInterval,
627✔
1057
    cache_hint: CacheHint,
627✔
1058
) -> Result<RasterTile2D<T>> {
627✔
1059
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
627✔
1060

1061
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
626✔
1062

1063
    let mut properties = RasterProperties::default();
624✔
1064

624✔
1065
    // always read the scale and offset values from the rasterband
624✔
1066
    properties_from_band(&mut properties, &rasterband);
624✔
1067

1068
    // read the properties from the dataset and rasterband metadata
1069
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
624✔
1070
        properties_from_gdal_metadata(&mut properties, dataset, properties_mapping);
4✔
1071
        properties_from_gdal_metadata(&mut properties, &rasterband, properties_mapping);
4✔
1072
    }
620✔
1073

1074
    // TODO: add cache_hint
1075
    Ok(RasterTile2D::new_with_tile_info_and_properties(
624✔
1076
        tile_time,
624✔
1077
        tile_info,
624✔
1078
        result_grid,
624✔
1079
        properties,
624✔
1080
        cache_hint,
624✔
1081
    ))
624✔
1082
}
627✔
1083

1084
fn create_no_data_tile<T: Pixel>(
81✔
1085
    tile_info: TileInformation,
81✔
1086
    tile_time: TimeInterval,
81✔
1087
    cache_hint: CacheHint,
81✔
1088
) -> RasterTile2D<T> {
81✔
1089
    // TODO: add cache_hint
81✔
1090
    RasterTile2D::new_with_tile_info_and_properties(
81✔
1091
        tile_time,
81✔
1092
        tile_info,
81✔
1093
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
81✔
1094
        RasterProperties::default(),
81✔
1095
        cache_hint,
81✔
1096
    )
81✔
1097
}
81✔
1098

1099
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14✔
1100
pub struct GdalMetadataMapping {
1101
    pub source_key: RasterPropertiesKey,
1102
    pub target_key: RasterPropertiesKey,
1103
    pub target_type: RasterPropertiesEntryType,
1104
}
1105

1106
impl GdalMetadataMapping {
1107
    pub fn identity(
×
1108
        key: RasterPropertiesKey,
×
1109
        target_type: RasterPropertiesEntryType,
×
1110
    ) -> GdalMetadataMapping {
×
1111
        GdalMetadataMapping {
×
1112
            source_key: key.clone(),
×
1113
            target_key: key,
×
1114
            target_type,
×
1115
        }
×
1116
    }
×
1117
}
1118

1119
fn properties_from_gdal_metadata<'a, I, M>(
8✔
1120
    properties: &mut RasterProperties,
8✔
1121
    gdal_dataset: &M,
8✔
1122
    properties_mapping: I,
8✔
1123
) where
8✔
1124
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1125
    M: GdalMetadata,
8✔
1126
{
8✔
1127
    let mapping_iter = properties_mapping.into_iter();
8✔
1128

1129
    for m in mapping_iter {
24✔
1130
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1131
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1132
        } else {
1133
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1134
        };
1135

1136
        if let Some(d) = data {
16✔
1137
            let entry = match m.target_type {
8✔
1138
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1139
                    |_| RasterPropertiesEntry::String(d),
×
1140
                    RasterPropertiesEntry::Number,
×
1141
                ),
×
1142
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1143
            };
1144

1145
            debug!(
8✔
1146
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1147
                &m.source_key, &m.target_key, &entry
×
1148
            );
1149

1150
            properties.insert_property(m.target_key.clone(), entry);
8✔
1151
        }
8✔
1152
    }
1153
}
8✔
1154

1155
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
1156
    if let Some(scale) = gdal_dataset.scale() {
624✔
1157
        properties.set_scale(scale);
1✔
1158
    };
623✔
1159
    if let Some(offset) = gdal_dataset.offset() {
624✔
1160
        properties.set_offset(offset);
1✔
1161
    };
623✔
1162

1163
    // ignore if there is no description
1164
    if let Ok(description) = gdal_dataset.description() {
624✔
1165
        properties.set_description(description);
624✔
1166
    }
624✔
1167
}
624✔
1168

1169
#[cfg(test)]
1170
mod tests {
1171
    use super::*;
1172
    use crate::engine::{MockExecutionContext, MockQueryContext};
1173
    use crate::test_data;
1174
    use crate::util::gdal::add_ndvi_dataset;
1175
    use crate::util::Result;
1176
    use geoengine_datatypes::hashmap;
1177
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1178
    use geoengine_datatypes::raster::{
1179
        EmptyGrid2D, GridBounds, GridIdx2D, TilesEqualIgnoringCacheHint,
1180
    };
1181
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1182
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1183
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1184
    use httptest::matchers::request;
1185
    use httptest::{responders, Expectation, Server};
1186

1187
    async fn query_gdal_source(
5✔
1188
        exe_ctx: &MockExecutionContext,
5✔
1189
        query_ctx: &MockQueryContext,
5✔
1190
        name: NamedData,
5✔
1191
        output_shape: GridShape2D,
5✔
1192
        output_bounds: SpatialPartition2D,
5✔
1193
        time_interval: TimeInterval,
5✔
1194
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1195
        let op = GdalSource {
5✔
1196
            params: GdalSourceParameters { data: name.clone() },
5✔
1197
        }
5✔
1198
        .boxed();
5✔
1199

5✔
1200
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1201
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1202
        let spatial_resolution =
5✔
1203
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1204

1205
        let o = op
5✔
1206
            .initialize(WorkflowOperatorPath::initialize_root(), exe_ctx)
5✔
1207
            .await
×
1208
            .unwrap();
5✔
1209

5✔
1210
        o.query_processor()
5✔
1211
            .unwrap()
5✔
1212
            .get_u8()
5✔
1213
            .unwrap()
5✔
1214
            .raster_query(
5✔
1215
                RasterQueryRectangle {
5✔
1216
                    spatial_bounds: output_bounds,
5✔
1217
                    time_interval,
5✔
1218
                    spatial_resolution,
5✔
1219
                },
5✔
1220
                query_ctx,
5✔
1221
            )
5✔
1222
            .await
×
1223
            .unwrap()
5✔
1224
            .collect()
5✔
1225
            .await
14✔
1226
    }
5✔
1227

1228
    fn load_ndvi_jan_2014(
3✔
1229
        output_shape: GridShape2D,
3✔
1230
        output_bounds: SpatialPartition2D,
3✔
1231
    ) -> Result<RasterTile2D<u8>> {
3✔
1232
        GdalRasterLoader::load_tile_data::<u8>(
3✔
1233
            &GdalDatasetParameters {
3✔
1234
                file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
3✔
1235
                rasterband_channel: 1,
3✔
1236
                geo_transform: GdalDatasetGeoTransform {
3✔
1237
                    origin_coordinate: (-180., 90.).into(),
3✔
1238
                    x_pixel_size: 0.1,
3✔
1239
                    y_pixel_size: -0.1,
3✔
1240
                },
3✔
1241
                width: 3600,
3✔
1242
                height: 1800,
3✔
1243
                file_not_found_handling: FileNotFoundHandling::NoData,
3✔
1244
                no_data_value: Some(0.),
3✔
1245
                properties_mapping: Some(vec![
3✔
1246
                    GdalMetadataMapping {
3✔
1247
                        source_key: RasterPropertiesKey {
3✔
1248
                            domain: None,
3✔
1249
                            key: "AREA_OR_POINT".to_string(),
3✔
1250
                        },
3✔
1251
                        target_type: RasterPropertiesEntryType::String,
3✔
1252
                        target_key: RasterPropertiesKey {
3✔
1253
                            domain: None,
3✔
1254
                            key: "AREA_OR_POINT".to_string(),
3✔
1255
                        },
3✔
1256
                    },
3✔
1257
                    GdalMetadataMapping {
3✔
1258
                        source_key: RasterPropertiesKey {
3✔
1259
                            domain: Some("IMAGE_STRUCTURE".to_string()),
3✔
1260
                            key: "COMPRESSION".to_string(),
3✔
1261
                        },
3✔
1262
                        target_type: RasterPropertiesEntryType::String,
3✔
1263
                        target_key: RasterPropertiesKey {
3✔
1264
                            domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
3✔
1265
                            key: "COMPRESSION".to_string(),
3✔
1266
                        },
3✔
1267
                    },
3✔
1268
                ]),
3✔
1269
                gdal_open_options: None,
3✔
1270
                gdal_config_options: None,
3✔
1271
                allow_alphaband_as_mask: true,
3✔
1272
                retry: None,
3✔
1273
            },
3✔
1274
            TileInformation::with_partition_and_shape(output_bounds, output_shape),
3✔
1275
            TimeInterval::default(),
3✔
1276
            CacheHint::default(),
3✔
1277
        )
3✔
1278
    }
3✔
1279

1280
    #[test]
1✔
1281
    fn tiling_strategy_origin() {
1✔
1282
        let tile_size_in_pixels = [600, 600];
1✔
1283
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1284
        let dataset_x_pixel_size = 0.1;
1✔
1285
        let dataset_y_pixel_size = -0.1;
1✔
1286
        let dataset_geo_transform = GeoTransform::new(
1✔
1287
            dataset_upper_right_coord,
1✔
1288
            dataset_x_pixel_size,
1✔
1289
            dataset_y_pixel_size,
1✔
1290
        );
1✔
1291

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

1✔
1294
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1295
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1296
            geo_transform: dataset_geo_transform,
1✔
1297
        };
1✔
1298

1✔
1299
        assert_eq!(
1✔
1300
            origin_split_tileing_strategy
1✔
1301
                .geo_transform
1✔
1302
                .upper_left_pixel_idx(&partition),
1✔
1303
            [0, 0].into()
1✔
1304
        );
1✔
1305
        assert_eq!(
1✔
1306
            origin_split_tileing_strategy
1✔
1307
                .geo_transform
1✔
1308
                .lower_right_pixel_idx(&partition),
1✔
1309
            [1800 - 1, 3600 - 1].into()
1✔
1310
        );
1✔
1311

1312
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1313
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1314
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1315
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1316
    }
1✔
1317

1318
    #[test]
1✔
1319
    fn tiling_strategy_zero() {
1✔
1320
        let tile_size_in_pixels = [600, 600];
1✔
1321
        let dataset_x_pixel_size = 0.1;
1✔
1322
        let dataset_y_pixel_size = -0.1;
1✔
1323
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1324
            0.0,
1✔
1325
            dataset_x_pixel_size,
1✔
1326
            0.0,
1✔
1327
            dataset_y_pixel_size,
1✔
1328
        );
1✔
1329

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

1✔
1332
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1333
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1334
            geo_transform: central_geo_transform,
1✔
1335
        };
1✔
1336

1✔
1337
        assert_eq!(
1✔
1338
            origin_split_tileing_strategy
1✔
1339
                .geo_transform
1✔
1340
                .upper_left_pixel_idx(&partition),
1✔
1341
            [-900, -1800].into()
1✔
1342
        );
1✔
1343
        assert_eq!(
1✔
1344
            origin_split_tileing_strategy
1✔
1345
                .geo_transform
1✔
1346
                .lower_right_pixel_idx(&partition),
1✔
1347
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1348
        );
1✔
1349

1350
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1351
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1352
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1353
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1354
    }
1✔
1355

1356
    #[test]
1✔
1357
    fn tile_idx_iterator() {
1✔
1358
        let tile_size_in_pixels = [600, 600];
1✔
1359
        let dataset_x_pixel_size = 0.1;
1✔
1360
        let dataset_y_pixel_size = -0.1;
1✔
1361
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1362
            0.0,
1✔
1363
            dataset_x_pixel_size,
1✔
1364
            0.0,
1✔
1365
            dataset_y_pixel_size,
1✔
1366
        );
1✔
1367

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

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

1✔
1375
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1376
            .tile_idx_iterator(partition)
1✔
1377
            .collect();
1✔
1378
        assert_eq!(vres.len(), 4 * 6);
1✔
1379
        assert_eq!(vres[0], [-2, -3].into());
1✔
1380
        assert_eq!(vres[1], [-2, -2].into());
1✔
1381
        assert_eq!(vres[2], [-2, -1].into());
1✔
1382
        assert_eq!(vres[23], [1, 2].into());
1✔
1383
    }
1✔
1384

1385
    #[test]
1✔
1386
    fn tile_information_iterator() {
1✔
1387
        let tile_size_in_pixels = [600, 600];
1✔
1388
        let dataset_x_pixel_size = 0.1;
1✔
1389
        let dataset_y_pixel_size = -0.1;
1✔
1390

1✔
1391
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1392
            0.0,
1✔
1393
            dataset_x_pixel_size,
1✔
1394
            0.0,
1✔
1395
            dataset_y_pixel_size,
1✔
1396
        );
1✔
1397

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

1✔
1400
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1401
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1402
            geo_transform: central_geo_transform,
1✔
1403
        };
1✔
1404

1✔
1405
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1406
            .tile_information_iterator(partition)
1✔
1407
            .collect();
1✔
1408
        assert_eq!(vres.len(), 4 * 6);
1✔
1409
        assert_eq!(
1✔
1410
            vres[0],
1✔
1411
            TileInformation::new(
1✔
1412
                [-2, -3].into(),
1✔
1413
                tile_size_in_pixels.into(),
1✔
1414
                central_geo_transform,
1✔
1415
            )
1✔
1416
        );
1✔
1417
        assert_eq!(
1✔
1418
            vres[1],
1✔
1419
            TileInformation::new(
1✔
1420
                [-2, -2].into(),
1✔
1421
                tile_size_in_pixels.into(),
1✔
1422
                central_geo_transform,
1✔
1423
            )
1✔
1424
        );
1✔
1425
        assert_eq!(
1✔
1426
            vres[12],
1✔
1427
            TileInformation::new(
1✔
1428
                [0, -3].into(),
1✔
1429
                tile_size_in_pixels.into(),
1✔
1430
                central_geo_transform,
1✔
1431
            )
1✔
1432
        );
1✔
1433
        assert_eq!(
1✔
1434
            vres[23],
1✔
1435
            TileInformation::new(
1✔
1436
                [1, 2].into(),
1✔
1437
                tile_size_in_pixels.into(),
1✔
1438
                central_geo_transform,
1✔
1439
            )
1✔
1440
        );
1✔
1441
    }
1✔
1442

1443
    #[test]
1✔
1444
    fn replace_time_placeholder() {
1✔
1445
        let params = GdalDatasetParameters {
1✔
1446
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1447
            rasterband_channel: 0,
1✔
1448
            geo_transform: TestDefault::test_default(),
1✔
1449
            width: 360,
1✔
1450
            height: 180,
1✔
1451
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1452
            no_data_value: Some(0.),
1✔
1453
            properties_mapping: None,
1✔
1454
            gdal_open_options: None,
1✔
1455
            gdal_config_options: None,
1✔
1456
            allow_alphaband_as_mask: true,
1✔
1457
            retry: None,
1✔
1458
        };
1✔
1459
        let replaced = params
1✔
1460
            .replace_time_placeholders(
1✔
1461
                &hashmap! {
1✔
1462
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1463
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1464
                        reference: TimeReference::Start,
1✔
1465
                    },
1✔
1466
                },
1✔
1467
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1468
            )
1✔
1469
            .unwrap();
1✔
1470
        assert_eq!(
1✔
1471
            replaced.file_path.to_string_lossy(),
1✔
1472
            "/foo/bar_022000000.tiff".to_string()
1✔
1473
        );
1✔
1474
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1475
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1476
        assert_eq!(params.width, replaced.width);
1✔
1477
        assert_eq!(params.height, replaced.height);
1✔
1478
        assert_eq!(
1✔
1479
            params.file_not_found_handling,
1✔
1480
            replaced.file_not_found_handling
1✔
1481
        );
1✔
1482
    }
1✔
1483

1484
    #[test]
1✔
1485
    fn test_load_tile_data() {
1✔
1486
        let output_shape: GridShape2D = [8, 8].into();
1✔
1487
        let output_bounds =
1✔
1488
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1489

1✔
1490
        let RasterTile2D {
1✔
1491
            global_geo_transform: _,
1✔
1492
            grid_array: grid,
1✔
1493
            tile_position: _,
1✔
1494
            time: _,
1✔
1495
            properties,
1✔
1496
            cache_hint: _,
1✔
1497
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1498

1✔
1499
        assert!(!grid.is_empty());
1✔
1500

1501
        let grid = grid.into_materialized_masked_grid();
1✔
1502

1✔
1503
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1504
        assert_eq!(
1✔
1505
            grid.inner_grid.data,
1✔
1506
            &[
1✔
1507
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1508
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1509
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1510
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1511
            ]
1✔
1512
        );
1✔
1513

1514
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1515
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1516

1517
        assert!((properties.scale_option()).is_none());
1✔
1518
        assert!(properties.offset_option().is_none());
1✔
1519
        assert_eq!(
1✔
1520
            properties.get_property(&RasterPropertiesKey {
1✔
1521
                key: "AREA_OR_POINT".to_string(),
1✔
1522
                domain: None,
1✔
1523
            }),
1✔
1524
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1525
        );
1✔
1526
        assert_eq!(
1✔
1527
            properties.get_property(&RasterPropertiesKey {
1✔
1528
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1529
                key: "COMPRESSION".to_string(),
1✔
1530
            }),
1✔
1531
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1532
        );
1✔
1533
    }
1✔
1534

1535
    #[test]
1✔
1536
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1537
        let output_shape: GridShape2D = [8, 8].into();
1✔
1538
        // shift world bbox one pixel up and to the left
1✔
1539
        let (x_size, y_size) = (45., 22.5);
1✔
1540
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1541
            (-180. - x_size, 90. + y_size).into(),
1✔
1542
            (180. - x_size, -90. + y_size).into(),
1✔
1543
        );
1✔
1544

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

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

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

1✔
1558
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1559
        assert_eq!(
1✔
1560
            x.inner_grid.data,
1✔
1561
            &[
1✔
1562
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1563
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1564
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1565
                255, 255, 255, 255, 255, 255
1✔
1566
            ]
1✔
1567
        );
1✔
1568
    }
1✔
1569

1570
    #[test]
1✔
1571
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1572
        let output_shape: GridShape2D = [8, 8].into();
1✔
1573
        // shift world bbox one pixel up and to the left
1✔
1574
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1575
        let output_bounds = SpatialPartition2D::new(
1✔
1576
            (-116.22222, 66.66666).into(),
1✔
1577
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1578
        )
1✔
1579
        .unwrap();
1✔
1580

1✔
1581
        let RasterTile2D {
1✔
1582
            global_geo_transform: _,
1✔
1583
            grid_array: grid,
1✔
1584
            tile_position: _,
1✔
1585
            time: _,
1✔
1586
            properties: _,
1✔
1587
            cache_hint: _,
1✔
1588
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1589

1✔
1590
        assert!(!grid.is_empty());
1✔
1591

1592
        let x = grid.into_materialized_masked_grid();
1✔
1593

1✔
1594
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1595
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1596
    }
1✔
1597

1598
    #[tokio::test]
1✔
1599
    async fn test_query_single_time_slice() {
1✔
1600
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1601
        let query_ctx = MockQueryContext::test_default();
1✔
1602
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1603

1✔
1604
        let output_shape: GridShape2D = [256, 256].into();
1✔
1605
        let output_bounds =
1✔
1606
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1607
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001); // 2014-01-01
1✔
1608

1609
        let c = query_gdal_source(
1✔
1610
            &exe_ctx,
1✔
1611
            &query_ctx,
1✔
1612
            id,
1✔
1613
            output_shape,
1✔
1614
            output_bounds,
1✔
1615
            time_interval,
1✔
1616
        )
1✔
1617
        .await;
5✔
1618
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1619

1✔
1620
        assert_eq!(c.len(), 4);
1✔
1621

1622
        assert_eq!(
1✔
1623
            c[0].time,
1✔
1624
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1625
        );
1✔
1626

1627
        assert_eq!(
1✔
1628
            c[0].tile_information().global_tile_position(),
1✔
1629
            [-1, -1].into()
1✔
1630
        );
1✔
1631

1632
        assert_eq!(
1✔
1633
            c[1].tile_information().global_tile_position(),
1✔
1634
            [-1, 0].into()
1✔
1635
        );
1✔
1636

1637
        assert_eq!(
1✔
1638
            c[2].tile_information().global_tile_position(),
1✔
1639
            [0, -1].into()
1✔
1640
        );
1✔
1641

1642
        assert_eq!(
1✔
1643
            c[3].tile_information().global_tile_position(),
1✔
1644
            [0, 0].into()
1✔
1645
        );
1✔
1646
    }
1647

1648
    #[tokio::test]
1✔
1649
    async fn test_query_multi_time_slices() {
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_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-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;
9✔
1668
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1669

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

1672
        assert_eq!(
1✔
1673
            c[0].time,
1✔
1674
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1675
        );
1✔
1676

1677
        assert_eq!(
1✔
1678
            c[5].time,
1✔
1679
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1680
        );
1✔
1681
    }
1682

1683
    #[tokio::test]
1✔
1684
    async fn test_query_before_data() {
1✔
1685
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1686
        let query_ctx = MockQueryContext::test_default();
1✔
1687
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1688

1✔
1689
        let output_shape: GridShape2D = [256, 256].into();
1✔
1690
        let output_bounds =
1✔
1691
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1692
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1693

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

1✔
1705
        assert_eq!(c.len(), 4);
1✔
1706

1707
        assert_eq!(
1✔
1708
            c[0].time,
1✔
1709
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1710
        );
1✔
1711
    }
1712

1713
    #[tokio::test]
1✔
1714
    async fn test_query_after_data() {
1✔
1715
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1716
        let query_ctx = MockQueryContext::test_default();
1✔
1717
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1718

1✔
1719
        let output_shape: GridShape2D = [256, 256].into();
1✔
1720
        let output_bounds =
1✔
1721
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1722
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1723

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

1✔
1735
        assert_eq!(c.len(), 4);
1✔
1736

1737
        assert_eq!(
1✔
1738
            c[0].time,
1✔
1739
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1740
        );
1✔
1741
    }
1742

1743
    #[tokio::test]
1✔
1744
    async fn test_nodata() {
1✔
1745
        hide_gdal_errors();
1✔
1746

1✔
1747
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1748
        let query_ctx = MockQueryContext::test_default();
1✔
1749
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1750

1✔
1751
        let output_shape: GridShape2D = [256, 256].into();
1✔
1752
        let output_bounds =
1✔
1753
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1754
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1755

1756
        let c = query_gdal_source(
1✔
1757
            &exe_ctx,
1✔
1758
            &query_ctx,
1✔
1759
            id,
1✔
1760
            output_shape,
1✔
1761
            output_bounds,
1✔
1762
            time_interval,
1✔
1763
        )
1✔
1764
        .await;
×
1765
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1766

1✔
1767
        assert_eq!(c.len(), 4);
1✔
1768

1769
        let tile_1 = &c[0];
1✔
1770

1✔
1771
        assert_eq!(
1✔
1772
            tile_1.time,
1✔
1773
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1774
        );
1✔
1775

1776
        assert!(tile_1.is_empty());
1✔
1777
    }
1778

1779
    #[tokio::test]
1✔
1780
    async fn timestep_without_params() {
1✔
1781
        let output_bounds =
1✔
1782
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1783
        let output_shape: GridShape2D = [256, 256].into();
1✔
1784

1✔
1785
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1786
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1787
        let params = None;
1✔
1788

1789
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
1790
            params,
1✔
1791
            tile_info,
1✔
1792
            time_interval,
1✔
1793
            CacheHint::default(),
1✔
1794
        )
1✔
1795
        .await;
×
1796

1797
        assert!(tile.is_ok());
1✔
1798

1799
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1800
            time_interval,
1✔
1801
            tile_info,
1✔
1802
            EmptyGrid2D::new(output_shape).into(),
1✔
1803
            CacheHint::default(),
1✔
1804
        );
1✔
1805

1✔
1806
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
1807
    }
1808

1809
    #[test]
1✔
1810
    fn it_reverts_config_options() {
1✔
1811
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1812

1✔
1813
        {
1✔
1814
            let _config =
1✔
1815
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1816

1✔
1817
            assert_eq!(
1✔
1818
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1819
                "bar".to_owned()
1✔
1820
            );
1✔
1821
        }
1822

1823
        assert_eq!(
1✔
1824
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1825
            String::new()
1✔
1826
        );
1✔
1827
    }
1✔
1828

1829
    #[test]
1✔
1830
    #[allow(clippy::too_many_lines)]
1831
    fn deserialize_dataset_parameters() {
1✔
1832
        let dataset_parameters = GdalDatasetParameters {
1✔
1833
            file_path: "path-to-data.tiff".into(),
1✔
1834
            rasterband_channel: 1,
1✔
1835
            geo_transform: GdalDatasetGeoTransform {
1✔
1836
                origin_coordinate: (-180., 90.).into(),
1✔
1837
                x_pixel_size: 0.1,
1✔
1838
                y_pixel_size: -0.1,
1✔
1839
            },
1✔
1840
            width: 3600,
1✔
1841
            height: 1800,
1✔
1842
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1843
            no_data_value: Some(f64::NAN),
1✔
1844
            properties_mapping: Some(vec![
1✔
1845
                GdalMetadataMapping {
1✔
1846
                    source_key: RasterPropertiesKey {
1✔
1847
                        domain: None,
1✔
1848
                        key: "AREA_OR_POINT".to_string(),
1✔
1849
                    },
1✔
1850
                    target_type: RasterPropertiesEntryType::String,
1✔
1851
                    target_key: RasterPropertiesKey {
1✔
1852
                        domain: None,
1✔
1853
                        key: "AREA_OR_POINT".to_string(),
1✔
1854
                    },
1✔
1855
                },
1✔
1856
                GdalMetadataMapping {
1✔
1857
                    source_key: RasterPropertiesKey {
1✔
1858
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
1859
                        key: "COMPRESSION".to_string(),
1✔
1860
                    },
1✔
1861
                    target_type: RasterPropertiesEntryType::String,
1✔
1862
                    target_key: RasterPropertiesKey {
1✔
1863
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1864
                        key: "COMPRESSION".to_string(),
1✔
1865
                    },
1✔
1866
                },
1✔
1867
            ]),
1✔
1868
            gdal_open_options: None,
1✔
1869
            gdal_config_options: None,
1✔
1870
            allow_alphaband_as_mask: true,
1✔
1871
            retry: None,
1✔
1872
        };
1✔
1873

1✔
1874
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1875

1✔
1876
        assert_eq!(
1✔
1877
            dataset_parameters_json,
1✔
1878
            serde_json::json!({
1✔
1879
                "filePath": "path-to-data.tiff",
1✔
1880
                "rasterbandChannel": 1,
1✔
1881
                "geoTransform": {
1✔
1882
                    "originCoordinate": {
1✔
1883
                        "x": -180.,
1✔
1884
                        "y": 90.
1✔
1885
                    },
1✔
1886
                    "xPixelSize": 0.1,
1✔
1887
                    "yPixelSize": -0.1
1✔
1888
                },
1✔
1889
                "width": 3600,
1✔
1890
                "height": 1800,
1✔
1891
                "fileNotFoundHandling": "NoData",
1✔
1892
                "noDataValue": "nan",
1✔
1893
                "propertiesMapping": [{
1✔
1894
                        "source_key": {
1✔
1895
                            "domain": null,
1✔
1896
                            "key": "AREA_OR_POINT"
1✔
1897
                        },
1✔
1898
                        "target_key": {
1✔
1899
                            "domain": null,
1✔
1900
                            "key": "AREA_OR_POINT"
1✔
1901
                        },
1✔
1902
                        "target_type": "String"
1✔
1903
                    },
1✔
1904
                    {
1✔
1905
                        "source_key": {
1✔
1906
                            "domain": "IMAGE_STRUCTURE",
1✔
1907
                            "key": "COMPRESSION"
1✔
1908
                        },
1✔
1909
                        "target_key": {
1✔
1910
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1911
                            "key": "COMPRESSION"
1✔
1912
                        },
1✔
1913
                        "target_type": "String"
1✔
1914
                    }
1✔
1915
                ],
1✔
1916
                "gdalOpenOptions": null,
1✔
1917
                "gdalConfigOptions": null,
1✔
1918
                "allowAlphabandAsMask": true,
1✔
1919
                "retry": null,
1✔
1920
            })
1✔
1921
        );
1✔
1922

1923
        let deserialized_parameters =
1✔
1924
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1925

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

1✔
1928
        assert_eq!(
1✔
1929
            deserialized_parameters.file_path,
1✔
1930
            dataset_parameters.file_path,
1✔
1931
        );
1✔
1932
        assert_eq!(
1✔
1933
            deserialized_parameters.rasterband_channel,
1✔
1934
            dataset_parameters.rasterband_channel,
1✔
1935
        );
1✔
1936
        assert_eq!(
1✔
1937
            deserialized_parameters.geo_transform,
1✔
1938
            dataset_parameters.geo_transform,
1✔
1939
        );
1✔
1940
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
1941
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
1942
        assert_eq!(
1✔
1943
            deserialized_parameters.file_not_found_handling,
1✔
1944
            dataset_parameters.file_not_found_handling,
1✔
1945
        );
1✔
1946
        assert!(
1✔
1947
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
1948
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
1949
        );
1950
        assert_eq!(
1✔
1951
            deserialized_parameters.properties_mapping,
1✔
1952
            dataset_parameters.properties_mapping,
1✔
1953
        );
1✔
1954
        assert_eq!(
1✔
1955
            deserialized_parameters.gdal_open_options,
1✔
1956
            dataset_parameters.gdal_open_options,
1✔
1957
        );
1✔
1958
        assert_eq!(
1✔
1959
            deserialized_parameters.gdal_config_options,
1✔
1960
            dataset_parameters.gdal_config_options,
1✔
1961
        );
1✔
1962
    }
1✔
1963

1964
    #[test]
1✔
1965
    fn gdal_geotransform_to_bounds_neg_y_0() {
1✔
1966
        let gt = GdalDatasetGeoTransform {
1✔
1967
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1968
            x_pixel_size: 1.,
1✔
1969
            y_pixel_size: -1.,
1✔
1970
        };
1✔
1971

1✔
1972
        let sb = gt.spatial_partition(10, 10);
1✔
1973

1✔
1974
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
1975
            .unwrap();
1✔
1976

1✔
1977
        assert_eq!(sb, exp);
1✔
1978
    }
1✔
1979

1980
    #[test]
1✔
1981
    fn gdal_geotransform_to_bounds_neg_y_5() {
1✔
1982
        let gt = GdalDatasetGeoTransform {
1✔
1983
            origin_coordinate: Coordinate2D::new(5., 5.),
1✔
1984
            x_pixel_size: 0.5,
1✔
1985
            y_pixel_size: -0.5,
1✔
1986
        };
1✔
1987

1✔
1988
        let sb = gt.spatial_partition(10, 10);
1✔
1989

1✔
1990
        let exp =
1✔
1991
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1✔
1992

1✔
1993
        assert_eq!(sb, exp);
1✔
1994
    }
1✔
1995

1996
    #[test]
1✔
1997
    fn gdal_geotransform_to_bounds_pos_y_0() {
1✔
1998
        let gt = GdalDatasetGeoTransform {
1✔
1999
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2000
            x_pixel_size: 1.,
1✔
2001
            y_pixel_size: 1.,
1✔
2002
        };
1✔
2003

1✔
2004
        let sb = gt.spatial_partition(10, 10);
1✔
2005

1✔
2006
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2007
            .unwrap();
1✔
2008

1✔
2009
        assert_eq!(sb, exp);
1✔
2010
    }
1✔
2011

2012
    #[test]
1✔
2013
    fn gdal_geotransform_to_bounds_pos_y_5() {
1✔
2014
        let gt = GdalDatasetGeoTransform {
1✔
2015
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
2016
            x_pixel_size: 0.5,
1✔
2017
            y_pixel_size: 0.5,
1✔
2018
        };
1✔
2019

1✔
2020
        let sb = gt.spatial_partition(10, 10);
1✔
2021

1✔
2022
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
2023
            .unwrap();
1✔
2024

1✔
2025
        assert_eq!(sb, exp);
1✔
2026
    }
1✔
2027

2028
    #[test]
1✔
2029
    fn gdal_read_window_data_origin_upper_left() {
1✔
2030
        let gt = GdalDatasetGeoTransform {
1✔
2031
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
2032
            x_pixel_size: 0.5,
1✔
2033
            y_pixel_size: -0.5,
1✔
2034
        };
1✔
2035

1✔
2036
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
2037
            .unwrap();
1✔
2038

1✔
2039
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2040

1✔
2041
        let exp = GdalReadWindow {
1✔
2042
            read_size_x: 4,
1✔
2043
            read_size_y: 6,
1✔
2044
            read_start_x: 6,
1✔
2045
            read_start_y: 4,
1✔
2046
        };
1✔
2047

1✔
2048
        assert_eq!(rw, exp);
1✔
2049
    }
1✔
2050

2051
    #[test]
1✔
2052
    fn gdal_read_window_data_origin_lower_left() {
1✔
2053
        let gt = GdalDatasetGeoTransform {
1✔
2054
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2055
            x_pixel_size: 1.,
1✔
2056
            y_pixel_size: 1.,
1✔
2057
        };
1✔
2058

1✔
2059
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2060
            .unwrap();
1✔
2061

1✔
2062
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2063

1✔
2064
        let exp = GdalReadWindow {
1✔
2065
            read_size_x: 10,
1✔
2066
            read_size_y: 10,
1✔
2067
            read_start_x: 0,
1✔
2068
            read_start_y: 0,
1✔
2069
        };
1✔
2070

1✔
2071
        assert_eq!(rw, exp);
1✔
2072
    }
1✔
2073

2074
    #[test]
1✔
2075
    fn read_up_side_down_raster() {
1✔
2076
        let output_shape: GridShape2D = [8, 8].into();
1✔
2077
        let output_bounds =
1✔
2078
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2079

1✔
2080
        let up_side_down_params = GdalDatasetParameters {
1✔
2081
            file_path: test_data!(
1✔
2082
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2083
            )
1✔
2084
            .into(),
1✔
2085
            rasterband_channel: 1,
1✔
2086
            geo_transform: GdalDatasetGeoTransform {
1✔
2087
                origin_coordinate: (-180., -90.).into(),
1✔
2088
                x_pixel_size: 0.1,
1✔
2089
                y_pixel_size: 0.1,
1✔
2090
            },
1✔
2091
            width: 3600,
1✔
2092
            height: 1800,
1✔
2093
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2094
            no_data_value: Some(0.),
1✔
2095
            properties_mapping: Some(vec![
1✔
2096
                GdalMetadataMapping {
1✔
2097
                    source_key: RasterPropertiesKey {
1✔
2098
                        domain: None,
1✔
2099
                        key: "AREA_OR_POINT".to_string(),
1✔
2100
                    },
1✔
2101
                    target_type: RasterPropertiesEntryType::String,
1✔
2102
                    target_key: RasterPropertiesKey {
1✔
2103
                        domain: None,
1✔
2104
                        key: "AREA_OR_POINT".to_string(),
1✔
2105
                    },
1✔
2106
                },
1✔
2107
                GdalMetadataMapping {
1✔
2108
                    source_key: RasterPropertiesKey {
1✔
2109
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2110
                        key: "COMPRESSION".to_string(),
1✔
2111
                    },
1✔
2112
                    target_type: RasterPropertiesEntryType::String,
1✔
2113
                    target_key: RasterPropertiesKey {
1✔
2114
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2115
                        key: "COMPRESSION".to_string(),
1✔
2116
                    },
1✔
2117
                },
1✔
2118
            ]),
1✔
2119
            gdal_open_options: None,
1✔
2120
            gdal_config_options: None,
1✔
2121
            allow_alphaband_as_mask: true,
1✔
2122
            retry: None,
1✔
2123
        };
1✔
2124

1✔
2125
        let tile_information =
1✔
2126
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2127

1✔
2128
        let RasterTile2D {
1✔
2129
            global_geo_transform: _,
1✔
2130
            grid_array: grid,
1✔
2131
            tile_position: _,
1✔
2132
            time: _,
1✔
2133
            properties,
1✔
2134
            cache_hint: _,
1✔
2135
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2136
            &up_side_down_params,
1✔
2137
            tile_information,
1✔
2138
            TimeInterval::default(),
1✔
2139
            CacheHint::default(),
1✔
2140
        )
1✔
2141
        .unwrap();
1✔
2142

1✔
2143
        assert!(!grid.is_empty());
1✔
2144

2145
        let grid = grid.into_materialized_masked_grid();
1✔
2146

1✔
2147
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2148
        assert_eq!(
1✔
2149
            grid.inner_grid.data,
1✔
2150
            &[
1✔
2151
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2152
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2153
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2154
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2155
            ]
1✔
2156
        );
1✔
2157

2158
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2159
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2160

2161
        assert!(properties.offset_option().is_none());
1✔
2162
        assert!(properties.scale_option().is_none());
1✔
2163
    }
1✔
2164

2165
    #[test]
1✔
2166
    fn read_raster_and_offset_scale() {
1✔
2167
        let output_shape: GridShape2D = [8, 8].into();
1✔
2168
        let output_bounds =
1✔
2169
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2170

1✔
2171
        let up_side_down_params = GdalDatasetParameters {
1✔
2172
            file_path: test_data!(
1✔
2173
                "raster/modis_ndvi/with_offset_scale/MOD13A2_M_NDVI_2014-01-01.TIFF"
1✔
2174
            )
1✔
2175
            .into(),
1✔
2176
            rasterband_channel: 1,
1✔
2177
            geo_transform: GdalDatasetGeoTransform {
1✔
2178
                origin_coordinate: (-180., -90.).into(),
1✔
2179
                x_pixel_size: 0.1,
1✔
2180
                y_pixel_size: 0.1,
1✔
2181
            },
1✔
2182
            width: 3600,
1✔
2183
            height: 1800,
1✔
2184
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2185
            no_data_value: Some(0.),
1✔
2186
            properties_mapping: None,
1✔
2187
            gdal_open_options: None,
1✔
2188
            gdal_config_options: None,
1✔
2189
            allow_alphaband_as_mask: true,
1✔
2190
            retry: None,
1✔
2191
        };
1✔
2192

1✔
2193
        let tile_information =
1✔
2194
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2195

1✔
2196
        let RasterTile2D {
1✔
2197
            global_geo_transform: _,
1✔
2198
            grid_array: grid,
1✔
2199
            tile_position: _,
1✔
2200
            time: _,
1✔
2201
            properties,
1✔
2202
            cache_hint: _,
1✔
2203
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2204
            &up_side_down_params,
1✔
2205
            tile_information,
1✔
2206
            TimeInterval::default(),
1✔
2207
            CacheHint::default(),
1✔
2208
        )
1✔
2209
        .unwrap();
1✔
2210

1✔
2211
        assert!(!grid.is_empty());
1✔
2212

2213
        let grid = grid.into_materialized_masked_grid();
1✔
2214

1✔
2215
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2216
        assert_eq!(
1✔
2217
            grid.inner_grid.data,
1✔
2218
            &[
1✔
2219
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2220
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2221
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2222
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2223
            ]
1✔
2224
        );
1✔
2225

2226
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2227
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2228

2229
        assert_eq!(properties.offset_option(), Some(37.));
1✔
2230
        assert_eq!(properties.scale_option(), Some(3.7));
1✔
2231

2232
        assert!(approx_eq!(f64, properties.offset(), 37.));
1✔
2233
        assert!(approx_eq!(f64, properties.scale(), 3.7));
1✔
2234
    }
1✔
2235

2236
    #[test]
1✔
2237
    #[allow(clippy::too_many_lines)]
2238
    fn it_creates_no_data_only_for_missing_files() {
1✔
2239
        hide_gdal_errors();
1✔
2240

1✔
2241
        let ds = GdalDatasetParameters {
1✔
2242
            file_path: "nonexisting_file.tif".into(),
1✔
2243
            rasterband_channel: 1,
1✔
2244
            geo_transform: TestDefault::test_default(),
1✔
2245
            width: 100,
1✔
2246
            height: 100,
1✔
2247
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2248
            no_data_value: None,
1✔
2249
            properties_mapping: None,
1✔
2250
            gdal_open_options: None,
1✔
2251
            gdal_config_options: None,
1✔
2252
            allow_alphaband_as_mask: true,
1✔
2253
            retry: None,
1✔
2254
        };
1✔
2255

1✔
2256
        let tile_info = TileInformation {
1✔
2257
            tile_size_in_pixels: [100, 100].into(),
1✔
2258
            global_tile_position: [0, 0].into(),
1✔
2259
            global_geo_transform: TestDefault::test_default(),
1✔
2260
        };
1✔
2261

1✔
2262
        let tile_time = TimeInterval::default();
1✔
2263

1✔
2264
        // file doesn't exist => no data
1✔
2265
        let result =
1✔
2266
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2267
                .unwrap();
1✔
2268
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2269

2270
        let ds = GdalDatasetParameters {
1✔
2271
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2272
            rasterband_channel: 100, // invalid channel
1✔
2273
            geo_transform: TestDefault::test_default(),
1✔
2274
            width: 100,
1✔
2275
            height: 100,
1✔
2276
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2277
            no_data_value: None,
1✔
2278
            properties_mapping: None,
1✔
2279
            gdal_open_options: None,
1✔
2280
            gdal_config_options: None,
1✔
2281
            allow_alphaband_as_mask: true,
1✔
2282
            retry: None,
1✔
2283
        };
1✔
2284

1✔
2285
        // invalid channel => error
1✔
2286
        let result =
1✔
2287
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2288
        assert!(result.is_err());
1✔
2289

2290
        let server = Server::run();
1✔
2291

1✔
2292
        server.expect(
1✔
2293
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2294
                .times(1)
1✔
2295
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2296
        );
1✔
2297

1✔
2298
        server.expect(
1✔
2299
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2300
                .times(1)
1✔
2301
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2302
        );
1✔
2303

1✔
2304
        let ds = GdalDatasetParameters {
1✔
2305
            file_path: format!("/vsicurl/{}", server.url_str("/non_existing.tif")).into(),
1✔
2306
            rasterband_channel: 1,
1✔
2307
            geo_transform: TestDefault::test_default(),
1✔
2308
            width: 100,
1✔
2309
            height: 100,
1✔
2310
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2311
            no_data_value: None,
1✔
2312
            properties_mapping: None,
1✔
2313
            gdal_open_options: None,
1✔
2314
            gdal_config_options: Some(vec![
1✔
2315
                (
1✔
2316
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2317
                    ".tif".to_owned(),
1✔
2318
                ),
1✔
2319
                (
1✔
2320
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2321
                    "EMPTY_DIR".to_owned(),
1✔
2322
                ),
1✔
2323
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2324
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2325
            ]),
1✔
2326
            allow_alphaband_as_mask: true,
1✔
2327
            retry: None,
1✔
2328
        };
1✔
2329

1✔
2330
        // 404 => no data
1✔
2331
        let result =
1✔
2332
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2333
                .unwrap();
1✔
2334
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2335

2336
        let ds = GdalDatasetParameters {
1✔
2337
            file_path: format!("/vsicurl/{}", server.url_str("/internal_error.tif")).into(),
1✔
2338
            rasterband_channel: 1,
1✔
2339
            geo_transform: TestDefault::test_default(),
1✔
2340
            width: 100,
1✔
2341
            height: 100,
1✔
2342
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2343
            no_data_value: None,
1✔
2344
            properties_mapping: None,
1✔
2345
            gdal_open_options: None,
1✔
2346
            gdal_config_options: Some(vec![
1✔
2347
                (
1✔
2348
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2349
                    ".tif".to_owned(),
1✔
2350
                ),
1✔
2351
                (
1✔
2352
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2353
                    "EMPTY_DIR".to_owned(),
1✔
2354
                ),
1✔
2355
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2356
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2357
            ]),
1✔
2358
            allow_alphaband_as_mask: true,
1✔
2359
            retry: None,
1✔
2360
        };
1✔
2361

1✔
2362
        // 500 => error
1✔
2363
        let result =
1✔
2364
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2365
        assert!(result.is_err());
1✔
2366
    }
1✔
2367

2368
    #[test]
1✔
2369
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2370
        hide_gdal_errors();
1✔
2371

1✔
2372
        let server = Server::run();
1✔
2373

1✔
2374
        server.expect(
1✔
2375
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2376
                .times(2)
1✔
2377
                .respond_with(responders::cycle![
1✔
2378
                    // first generic error
1✔
2379
                    responders::status_code(500),
1✔
2380
                    // then 404 file not found
1✔
2381
                    responders::status_code(404)
1✔
2382
                ]),
1✔
2383
        );
1✔
2384

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

1✔
2387
        let options = Some(vec![
1✔
2388
            (
1✔
2389
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2390
                ".tif".to_owned(),
1✔
2391
            ),
1✔
2392
            (
1✔
2393
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2394
                "EMPTY_DIR".to_owned(),
1✔
2395
            ),
1✔
2396
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2397
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2398
        ]);
1✔
2399

1✔
2400
        let _thread_local_configs = options
1✔
2401
            .as_ref()
1✔
2402
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2403

1✔
2404
        // first fail
1✔
2405
        let result = gdal_open_dataset_ex(
1✔
2406
            file_path.as_path(),
1✔
2407
            DatasetOptions {
1✔
2408
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2409
                ..DatasetOptions::default()
1✔
2410
            },
1✔
2411
        );
1✔
2412

1✔
2413
        // it failed, but not with file not found
1✔
2414
        assert!(result.is_err());
1✔
2415
        if let Err(error) = result {
1✔
2416
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2417
        }
×
2418

2419
        // second fail doesn't even try, so still not "file not found", even though it should be now
2420
        let result = gdal_open_dataset_ex(
1✔
2421
            file_path.as_path(),
1✔
2422
            DatasetOptions {
1✔
2423
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2424
                ..DatasetOptions::default()
1✔
2425
            },
1✔
2426
        );
1✔
2427

1✔
2428
        assert!(result.is_err());
1✔
2429
        if let Err(error) = result {
1✔
2430
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2431
        }
×
2432

2433
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2434

1✔
2435
        // after clearing the cache, it tries again
1✔
2436
        let result = gdal_open_dataset_ex(
1✔
2437
            file_path.as_path(),
1✔
2438
            DatasetOptions {
1✔
2439
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2440
                ..DatasetOptions::default()
1✔
2441
            },
1✔
2442
        );
1✔
2443

1✔
2444
        // now we get the file not found error
1✔
2445
        assert!(result.is_err());
1✔
2446
        if let Err(error) = result {
1✔
2447
            assert!(error_is_gdal_file_not_found(&error));
1✔
2448
        }
×
2449
    }
1✔
2450

2451
    #[tokio::test]
1✔
2452
    async fn it_attaches_cache_hint() {
1✔
2453
        let output_bounds =
1✔
2454
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
2455
        let output_shape: GridShape2D = [256, 256].into();
1✔
2456

1✔
2457
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2458
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
2459
        let params = None;
1✔
2460

2461
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
2462
            params,
1✔
2463
            tile_info,
1✔
2464
            time_interval,
1✔
2465
            CacheHint::seconds(1234),
1✔
2466
        )
1✔
2467
        .await;
×
2468

2469
        assert!(tile.is_ok());
1✔
2470

2471
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
2472
            time_interval,
1✔
2473
            tile_info,
1✔
2474
            EmptyGrid2D::new(output_shape).into(),
1✔
2475
            CacheHint::seconds(1234),
1✔
2476
        );
1✔
2477

1✔
2478
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
2479
    }
2480
}
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