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

geo-engine / geoengine / 7006568925

27 Nov 2023 02:07PM UTC coverage: 89.651% (+0.2%) from 89.498%
7006568925

push

github

web-flow
Merge pull request #888 from geo-engine/raster_stacks

raster stacking

4032 of 4274 new or added lines in 107 files covered. (94.34%)

12 existing lines in 8 files now uncovered.

113020 of 126066 relevant lines covered (89.65%)

59901.79 hits per line

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

94.26
/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::{
31
    AxisAlignedRectangle, Coordinate2D, DateTimeParseFormat, RasterQueryRectangle,
32
    SpatialPartition2D, SpatialPartitioned,
33
};
34
use geoengine_datatypes::primitives::{BandSelection, CacheHint};
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 postgres_types::{FromSql, ToSql};
54
use serde::{Deserialize, Serialize};
55
use snafu::{ensure, ResultExt};
56
use std::collections::HashMap;
57
use std::convert::TryFrom;
58
use std::ffi::CString;
59
use std::marker::PhantomData;
60
use std::path::{Path, PathBuf};
61
use std::time::Instant;
62

63
mod db_types;
64
mod error;
65
mod loading_info;
66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1,630✔
224
        [grid_y_index, grid_x_index].into()
1,630✔
225
    }
1,630✔
226

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

236
        /*
237
        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:
238

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

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

252
        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.
253

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

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

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

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

286
        debug_assert!(near_idx_x <= far_idx_x);
815✔
287
        debug_assert!(near_idx_y <= far_idx_y);
815✔
288

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

428
struct GdalRasterLoader {}
429

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

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

455
                async move {
812✔
456
                    let load_tile_result = crate::util::spawn_blocking(move || {
812✔
457
                        Self::load_tile_data(&ds, tile_information, tile_time, cache_hint)
812✔
458
                    })
812✔
459
                    .await
769✔
460
                    .context(crate::error::TokioJoin);
797✔
461

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

471
                            Err(e)
6✔
472
                        }
473
                    }
474
                }
797✔
475
            },
812✔
476
        )
806✔
477
        .await
775✔
478
    }
791✔
479

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

502
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
33✔
503
            }
504

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

511
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
46✔
512
            }
513
        }
514
    }
870✔
515

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

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

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

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

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

553
        if let Err(error) = &dataset_result {
821✔
554
            let is_file_not_found = error_is_gdal_file_not_found(error);
7✔
555

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

814✔
575
        let dataset = dataset_result.expect("checked");
814✔
576

577
        let result_tile = read_raster_tile_with_properties(
814✔
578
            &dataset,
814✔
579
            dataset_params,
814✔
580
            tile_information,
814✔
581
            tile_time,
814✔
582
            cache_hint,
814✔
583
        )?;
814✔
584

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

588
        Ok(result_tile)
811✔
589
    }
821✔
590

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

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

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

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

649
impl<T> GdalSourceProcessor<T> where T: gdal::raster::GdalType + Pixel {}
650

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

660
    async fn _query<'a>(
109✔
661
        &'a self,
109✔
662
        query: RasterQueryRectangle,
109✔
663
        _ctx: &'a dyn crate::engine::QueryContext,
109✔
664
    ) -> Result<BoxStream<Result<Self::Output>>> {
109✔
665
        ensure!(
109✔
666
            query.attributes.as_slice() == [0],
109✔
NEW
667
            crate::error::GdalSourceDoesNotSupportQueryingOtherBandsThanTheFirstOneYet
×
668
        );
669

670
        let start = Instant::now();
109✔
671
        debug!(
109✔
672
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
×
673
            P::TYPE,
×
674
            &query
×
675
        );
676

677
        debug!(
109✔
678
            "GdalSourceProcessor<{:?}> meta data loaded, took {:?}.",
×
679
            P::TYPE,
×
680
            start.elapsed()
×
681
        );
682

683
        let spatial_resolution = query.spatial_resolution;
109✔
684

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

698
        let tiling_strategy = self
109✔
699
            .tiling_specification
109✔
700
            .strategy(pixel_size_x, pixel_size_y);
109✔
701

702
        let result_descriptor = self.meta_data.result_descriptor().await?;
109✔
703

704
        let mut empty = false;
109✔
705
        debug!("result descr bbox: {:?}", result_descriptor.bbox);
109✔
706
        debug!("query bbox: {:?}", query.spatial_bounds);
109✔
707

708
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
109✔
709
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
70✔
710
                debug!("query does not intersect spatial data bounds");
2✔
711
                empty = true;
2✔
712
            }
68✔
713
        }
39✔
714

715
        // TODO: use the time bounds to early return.
716
        /*
717
        if let Some(data_time_bounds) = result_descriptor.time {
718
            if !data_time_bounds.intersects(&query.time_interval) {
719
                debug!("query does not intersect temporal data bounds");
720
                empty = true;
721
            }
722
        }
723
        */
724

725
        let loading_iter = if empty {
109✔
726
            GdalLoadingInfoTemporalSliceIterator::Static {
2✔
727
                parts: vec![].into_iter(),
2✔
728
            }
2✔
729
        } else {
730
            let loading_info = self.meta_data.loading_info(query.clone()).await?;
107✔
731
            loading_info.info
107✔
732
        };
733

734
        let source_stream = stream::iter(loading_iter);
109✔
735

109✔
736
        let source_stream = GdalRasterLoader::loading_info_to_tile_stream(
109✔
737
            source_stream,
109✔
738
            query.clone(),
109✔
739
            tiling_strategy,
109✔
740
        );
109✔
741

109✔
742
        // use SparseTilesFillAdapter to fill all the gaps
109✔
743
        let filled_stream = SparseTilesFillAdapter::new(
109✔
744
            source_stream,
109✔
745
            tiling_strategy.tile_grid_box(query.spatial_partition()),
109✔
746
            query.attributes.count(),
109✔
747
            tiling_strategy.geo_transform,
109✔
748
            tiling_strategy.tile_size_in_pixels,
109✔
749
            FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles,
109✔
750
        );
109✔
751
        Ok(filled_stream.boxed())
109✔
752
    }
218✔
753
}
754

755
pub type GdalSource = SourceOperator<GdalSourceParameters>;
756

757
impl OperatorName for GdalSource {
758
    const TYPE_NAME: &'static str = "GdalSource";
759
}
760

761
#[typetag::serde]
32✔
762
#[async_trait]
763
impl RasterOperator for GdalSource {
764
    async fn _initialize(
49✔
765
        self: Box<Self>,
49✔
766
        path: WorkflowOperatorPath,
49✔
767
        context: &dyn crate::engine::ExecutionContext,
49✔
768
    ) -> Result<Box<dyn InitializedRasterOperator>> {
49✔
769
        let data_id = context.resolve_named_data(&self.params.data).await?;
68✔
770
        let meta_data: GdalMetaData = context.meta_data(&data_id).await?;
80✔
771

772
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
48✔
773
        debug!("GdalSource path: {:?}", path);
48✔
774

775
        let op = InitializedGdalSourceOperator {
48✔
776
            name: CanonicOperatorName::from(&self),
48✔
777
            result_descriptor: meta_data.result_descriptor().await?,
48✔
778
            meta_data,
48✔
779
            tiling_specification: context.tiling_specification(),
48✔
780
        };
48✔
781

48✔
782
        Ok(op.boxed())
48✔
783
    }
98✔
784

785
    span_fn!(GdalSource);
1✔
786
}
787

788
pub struct InitializedGdalSourceOperator {
789
    name: CanonicOperatorName,
790
    pub meta_data: GdalMetaData,
791
    pub result_descriptor: RasterResultDescriptor,
792
    pub tiling_specification: TilingSpecification,
793
}
794

795
impl InitializedRasterOperator for InitializedGdalSourceOperator {
796
    fn result_descriptor(&self) -> &RasterResultDescriptor {
84✔
797
        &self.result_descriptor
84✔
798
    }
84✔
799

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

876
    fn canonic_name(&self) -> CanonicOperatorName {
1✔
877
        self.name.clone()
1✔
878
    }
1✔
879
}
880

881
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
882
/// It fails if the tile is not within the dataset.
883
#[allow(clippy::float_cmp)]
884
fn read_grid_from_raster<T, D>(
813✔
885
    rasterband: &GdalRasterBand,
813✔
886
    read_window: &GdalReadWindow,
813✔
887
    out_shape: D,
813✔
888
    dataset_params: &GdalDatasetParameters,
813✔
889
    flip_y_axis: bool,
813✔
890
) -> Result<GridOrEmpty<D, T>>
813✔
891
where
813✔
892
    T: Pixel + GdalType + Default + FromPrimitive,
813✔
893
    D: Clone + GridSize + PartialEq,
813✔
894
{
813✔
895
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
813✔
896

897
    let buffer = rasterband.read_as::<T>(
813✔
898
        read_window.gdal_window_start(), // pixelspace origin
813✔
899
        read_window.gdal_window_size(),  // pixelspace size
813✔
900
        gdal_out_shape,                  // requested raster size
813✔
901
        None,                            // sampling mode
813✔
902
    )?;
813✔
903
    let data_grid = Grid::new(out_shape.clone(), buffer.data)?;
811✔
904

905
    let data_grid = if flip_y_axis {
811✔
906
        data_grid.reversed_y_axis_grid()
1✔
907
    } else {
908
        data_grid
810✔
909
    };
910

911
    let dataset_mask_flags = rasterband.mask_flags()?;
811✔
912

913
    if dataset_mask_flags.is_all_valid() {
811✔
914
        debug!("all pixels are valid --> skip no-data and mask handling.");
×
915
        return Ok(MaskedGrid::new_with_data(data_grid).into());
×
916
    }
811✔
917

811✔
918
    if dataset_mask_flags.is_nodata() {
811✔
919
        debug!("raster uses a no-data value --> use no-data handling.");
803✔
920
        let no_data_value = dataset_params
803✔
921
            .no_data_value
803✔
922
            .or_else(|| rasterband.no_data_value())
803✔
923
            .and_then(FromPrimitive::from_f64);
803✔
924
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
803✔
925
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
803✔
926
        return Ok(grid_or_empty);
803✔
927
    }
8✔
928

8✔
929
    if dataset_mask_flags.is_alpha() {
8✔
930
        debug!("raster uses alpha band to mask pixels.");
×
931
        if !dataset_params.allow_alphaband_as_mask {
×
932
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
933
        }
×
934
    }
8✔
935

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

938
    let mask_band = rasterband.open_mask_band()?;
8✔
939
    let mask_buffer = mask_band.read_as::<u8>(
8✔
940
        read_window.gdal_window_start(), // pixelspace origin
8✔
941
        read_window.gdal_window_size(),  // pixelspace size
8✔
942
        gdal_out_shape,                  // requested raster size
8✔
943
        None,                            // sampling mode
8✔
944
    )?;
8✔
945
    let mask_grid = Grid::new(out_shape, mask_buffer.data)?.map_elements(|p: u8| p > 0);
360,016✔
946

947
    let mask_grid = if flip_y_axis {
8✔
948
        mask_grid.reversed_y_axis_grid()
×
949
    } else {
950
        mask_grid
8✔
951
    };
952

953
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
8✔
954
    Ok(GridOrEmpty::from(masked_grid))
8✔
955
}
813✔
956

957
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
958
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
959
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
960
fn read_partial_grid_from_raster<T>(
469✔
961
    rasterband: &GdalRasterBand,
469✔
962
    gdal_read_window: &GdalReadWindow,
469✔
963
    out_tile_read_bounds: GridBoundingBox2D,
469✔
964
    out_tile_shape: GridShape2D,
469✔
965
    dataset_params: &GdalDatasetParameters,
469✔
966
    flip_y_axis: bool,
469✔
967
) -> Result<GridOrEmpty2D<T>>
469✔
968
where
469✔
969
    T: Pixel + GdalType + Default + FromPrimitive,
469✔
970
{
469✔
971
    let dataset_raster = read_grid_from_raster(
469✔
972
        rasterband,
469✔
973
        gdal_read_window,
469✔
974
        out_tile_read_bounds,
469✔
975
        dataset_params,
469✔
976
        flip_y_axis,
469✔
977
    )?;
469✔
978

979
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
467✔
980
    tile_raster.grid_blit_from(&dataset_raster);
467✔
981
    Ok(tile_raster)
467✔
982
}
469✔
983

984
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
985
/// It handles conversion to grid coordinates.
986
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
987
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
988
fn read_grid_and_handle_edges<T>(
813✔
989
    tile_info: TileInformation,
813✔
990
    dataset: &GdalDataset,
813✔
991
    rasterband: &GdalRasterBand,
813✔
992
    dataset_params: &GdalDatasetParameters,
813✔
993
) -> Result<GridOrEmpty2D<T>>
813✔
994
where
813✔
995
    T: Pixel + GdalType + Default + FromPrimitive,
813✔
996
{
813✔
997
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
813✔
998
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
813✔
999

813✔
1000
    if !approx_eq!(
813✔
1001
        GdalDatasetGeoTransform,
813✔
1002
        gdal_dataset_geotransform,
813✔
1003
        dataset_params.geo_transform
813✔
1004
    ) {
813✔
1005
        log::warn!(
1✔
1006
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
1007
            dataset_params.geo_transform,
1008
            gdal_dataset_geotransform,
1009
        );
1010
    };
812✔
1011

1012
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
813✔
1013
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
813✔
1014

1015
    let gdal_dataset_bounds =
813✔
1016
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
813✔
1017

813✔
1018
    let output_bounds = tile_info.spatial_partition();
813✔
1019
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
813✔
1020
    let output_shape = tile_info.tile_size_in_pixels();
813✔
1021

1022
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
813✔
1023
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
1024
    };
1025

1026
    let tile_geo_transform = tile_info.tile_geo_transform();
813✔
1027

813✔
1028
    let gdal_read_window =
813✔
1029
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
813✔
1030

813✔
1031
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
813✔
1032
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
813✔
1033

813✔
1034
    if is_y_axis_flipped {
813✔
1035
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1036
    }
812✔
1037

1038
    let result_grid = if dataset_intersection_area == output_bounds {
813✔
1039
        read_grid_from_raster(
344✔
1040
            rasterband,
344✔
1041
            &gdal_read_window,
344✔
1042
            output_shape,
344✔
1043
            dataset_params,
344✔
1044
            is_y_axis_flipped,
344✔
1045
        )?
344✔
1046
    } else {
1047
        let partial_tile_grid_bounds =
469✔
1048
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
469✔
1049

469✔
1050
        read_partial_grid_from_raster(
469✔
1051
            rasterband,
469✔
1052
            &gdal_read_window,
469✔
1053
            partial_tile_grid_bounds,
469✔
1054
            output_shape,
469✔
1055
            dataset_params,
469✔
1056
            is_y_axis_flipped,
469✔
1057
        )?
469✔
1058
    };
1059

1060
    Ok(result_grid)
811✔
1061
}
813✔
1062

1063
/// 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.
1064
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
814✔
1065
    dataset: &GdalDataset,
814✔
1066
    dataset_params: &GdalDatasetParameters,
814✔
1067
    tile_info: TileInformation,
814✔
1068
    tile_time: TimeInterval,
814✔
1069
    cache_hint: CacheHint,
814✔
1070
) -> Result<RasterTile2D<T>> {
814✔
1071
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
814✔
1072

1073
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
813✔
1074

1075
    let mut properties = RasterProperties::default();
811✔
1076

811✔
1077
    // always read the scale and offset values from the rasterband
811✔
1078
    properties_from_band(&mut properties, &rasterband);
811✔
1079

1080
    // read the properties from the dataset and rasterband metadata
1081
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
811✔
1082
        properties_from_gdal_metadata(&mut properties, dataset, properties_mapping);
4✔
1083
        properties_from_gdal_metadata(&mut properties, &rasterband, properties_mapping);
4✔
1084
    }
807✔
1085

1086
    // TODO: add cache_hint
1087
    Ok(RasterTile2D::new_with_tile_info_and_properties(
811✔
1088
        tile_time,
811✔
1089
        tile_info,
811✔
1090
        0,
811✔
1091
        result_grid,
811✔
1092
        properties,
811✔
1093
        cache_hint,
811✔
1094
    ))
811✔
1095
}
814✔
1096

1097
fn create_no_data_tile<T: Pixel>(
81✔
1098
    tile_info: TileInformation,
81✔
1099
    tile_time: TimeInterval,
81✔
1100
    cache_hint: CacheHint,
81✔
1101
) -> RasterTile2D<T> {
81✔
1102
    // TODO: add cache_hint
81✔
1103
    RasterTile2D::new_with_tile_info_and_properties(
81✔
1104
        tile_time,
81✔
1105
        tile_info,
81✔
1106
        0,
81✔
1107
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
81✔
1108
        RasterProperties::default(),
81✔
1109
        cache_hint,
81✔
1110
    )
81✔
1111
}
81✔
1112

1113
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
2,364✔
1114
pub struct GdalMetadataMapping {
1115
    pub source_key: RasterPropertiesKey,
1116
    pub target_key: RasterPropertiesKey,
1117
    pub target_type: RasterPropertiesEntryType,
1118
}
1119

1120
impl GdalMetadataMapping {
1121
    pub fn identity(
×
1122
        key: RasterPropertiesKey,
×
1123
        target_type: RasterPropertiesEntryType,
×
1124
    ) -> GdalMetadataMapping {
×
1125
        GdalMetadataMapping {
×
1126
            source_key: key.clone(),
×
1127
            target_key: key,
×
1128
            target_type,
×
1129
        }
×
1130
    }
×
1131
}
1132

1133
fn properties_from_gdal_metadata<'a, I, M>(
8✔
1134
    properties: &mut RasterProperties,
8✔
1135
    gdal_dataset: &M,
8✔
1136
    properties_mapping: I,
8✔
1137
) where
8✔
1138
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1139
    M: GdalMetadata,
8✔
1140
{
8✔
1141
    let mapping_iter = properties_mapping.into_iter();
8✔
1142

1143
    for m in mapping_iter {
24✔
1144
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1145
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1146
        } else {
1147
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1148
        };
1149

1150
        if let Some(d) = data {
16✔
1151
            let entry = match m.target_type {
8✔
1152
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1153
                    |_| RasterPropertiesEntry::String(d),
×
1154
                    RasterPropertiesEntry::Number,
×
1155
                ),
×
1156
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1157
            };
1158

1159
            debug!(
8✔
1160
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1161
                &m.source_key, &m.target_key, &entry
×
1162
            );
1163

1164
            properties.insert_property(m.target_key.clone(), entry);
8✔
1165
        }
8✔
1166
    }
1167
}
8✔
1168

1169
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
1170
    if let Some(scale) = gdal_dataset.scale() {
811✔
1171
        properties.set_scale(scale);
1✔
1172
    };
810✔
1173
    if let Some(offset) = gdal_dataset.offset() {
811✔
1174
        properties.set_offset(offset);
1✔
1175
    };
810✔
1176

1177
    // ignore if there is no description
1178
    if let Ok(description) = gdal_dataset.description() {
811✔
1179
        properties.set_description(description);
811✔
1180
    }
811✔
1181
}
811✔
1182

1183
#[cfg(test)]
1184
mod tests {
1185
    use super::*;
1186
    use crate::engine::{MockExecutionContext, MockQueryContext};
1187
    use crate::test_data;
1188
    use crate::util::gdal::add_ndvi_dataset;
1189
    use crate::util::Result;
1190
    use geoengine_datatypes::hashmap;
1191
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1192
    use geoengine_datatypes::raster::{
1193
        EmptyGrid2D, GridBounds, GridIdx2D, TilesEqualIgnoringCacheHint,
1194
    };
1195
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1196
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1197
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1198
    use httptest::matchers::request;
1199
    use httptest::{responders, Expectation, Server};
1200

1201
    async fn query_gdal_source(
5✔
1202
        exe_ctx: &MockExecutionContext,
5✔
1203
        query_ctx: &MockQueryContext,
5✔
1204
        name: NamedData,
5✔
1205
        output_shape: GridShape2D,
5✔
1206
        output_bounds: SpatialPartition2D,
5✔
1207
        time_interval: TimeInterval,
5✔
1208
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1209
        let op = GdalSource {
5✔
1210
            params: GdalSourceParameters { data: name.clone() },
5✔
1211
        }
5✔
1212
        .boxed();
5✔
1213

5✔
1214
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1215
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1216
        let spatial_resolution =
5✔
1217
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1218

1219
        let o = op
5✔
1220
            .initialize(WorkflowOperatorPath::initialize_root(), exe_ctx)
5✔
1221
            .await
×
1222
            .unwrap();
5✔
1223

5✔
1224
        o.query_processor()
5✔
1225
            .unwrap()
5✔
1226
            .get_u8()
5✔
1227
            .unwrap()
5✔
1228
            .raster_query(
5✔
1229
                RasterQueryRectangle {
5✔
1230
                    spatial_bounds: output_bounds,
5✔
1231
                    time_interval,
5✔
1232
                    spatial_resolution,
5✔
1233
                    attributes: BandSelection::first(),
5✔
1234
                },
5✔
1235
                query_ctx,
5✔
1236
            )
5✔
1237
            .await
×
1238
            .unwrap()
5✔
1239
            .collect()
5✔
1240
            .await
14✔
1241
    }
5✔
1242

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

1295
    #[test]
1✔
1296
    fn tiling_strategy_origin() {
1✔
1297
        let tile_size_in_pixels = [600, 600];
1✔
1298
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1299
        let dataset_x_pixel_size = 0.1;
1✔
1300
        let dataset_y_pixel_size = -0.1;
1✔
1301
        let dataset_geo_transform = GeoTransform::new(
1✔
1302
            dataset_upper_right_coord,
1✔
1303
            dataset_x_pixel_size,
1✔
1304
            dataset_y_pixel_size,
1✔
1305
        );
1✔
1306

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

1✔
1309
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1310
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1311
            geo_transform: dataset_geo_transform,
1✔
1312
        };
1✔
1313

1✔
1314
        assert_eq!(
1✔
1315
            origin_split_tileing_strategy
1✔
1316
                .geo_transform
1✔
1317
                .upper_left_pixel_idx(&partition),
1✔
1318
            [0, 0].into()
1✔
1319
        );
1✔
1320
        assert_eq!(
1✔
1321
            origin_split_tileing_strategy
1✔
1322
                .geo_transform
1✔
1323
                .lower_right_pixel_idx(&partition),
1✔
1324
            [1800 - 1, 3600 - 1].into()
1✔
1325
        );
1✔
1326

1327
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1328
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1329
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1330
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1331
    }
1✔
1332

1333
    #[test]
1✔
1334
    fn tiling_strategy_zero() {
1✔
1335
        let tile_size_in_pixels = [600, 600];
1✔
1336
        let dataset_x_pixel_size = 0.1;
1✔
1337
        let dataset_y_pixel_size = -0.1;
1✔
1338
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1339
            0.0,
1✔
1340
            dataset_x_pixel_size,
1✔
1341
            0.0,
1✔
1342
            dataset_y_pixel_size,
1✔
1343
        );
1✔
1344

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

1✔
1347
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1348
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1349
            geo_transform: central_geo_transform,
1✔
1350
        };
1✔
1351

1✔
1352
        assert_eq!(
1✔
1353
            origin_split_tileing_strategy
1✔
1354
                .geo_transform
1✔
1355
                .upper_left_pixel_idx(&partition),
1✔
1356
            [-900, -1800].into()
1✔
1357
        );
1✔
1358
        assert_eq!(
1✔
1359
            origin_split_tileing_strategy
1✔
1360
                .geo_transform
1✔
1361
                .lower_right_pixel_idx(&partition),
1✔
1362
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1363
        );
1✔
1364

1365
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1366
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1367
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1368
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1369
    }
1✔
1370

1371
    #[test]
1✔
1372
    fn tile_idx_iterator() {
1✔
1373
        let tile_size_in_pixels = [600, 600];
1✔
1374
        let dataset_x_pixel_size = 0.1;
1✔
1375
        let dataset_y_pixel_size = -0.1;
1✔
1376
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1377
            0.0,
1✔
1378
            dataset_x_pixel_size,
1✔
1379
            0.0,
1✔
1380
            dataset_y_pixel_size,
1✔
1381
        );
1✔
1382

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

1✔
1385
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1386
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1387
            geo_transform: central_geo_transform,
1✔
1388
        };
1✔
1389

1✔
1390
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1391
            .tile_idx_iterator(partition)
1✔
1392
            .collect();
1✔
1393
        assert_eq!(vres.len(), 4 * 6);
1✔
1394
        assert_eq!(vres[0], [-2, -3].into());
1✔
1395
        assert_eq!(vres[1], [-2, -2].into());
1✔
1396
        assert_eq!(vres[2], [-2, -1].into());
1✔
1397
        assert_eq!(vres[23], [1, 2].into());
1✔
1398
    }
1✔
1399

1400
    #[test]
1✔
1401
    fn tile_information_iterator() {
1✔
1402
        let tile_size_in_pixels = [600, 600];
1✔
1403
        let dataset_x_pixel_size = 0.1;
1✔
1404
        let dataset_y_pixel_size = -0.1;
1✔
1405

1✔
1406
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1407
            0.0,
1✔
1408
            dataset_x_pixel_size,
1✔
1409
            0.0,
1✔
1410
            dataset_y_pixel_size,
1✔
1411
        );
1✔
1412

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

1✔
1415
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1416
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1417
            geo_transform: central_geo_transform,
1✔
1418
        };
1✔
1419

1✔
1420
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1421
            .tile_information_iterator(partition)
1✔
1422
            .collect();
1✔
1423
        assert_eq!(vres.len(), 4 * 6);
1✔
1424
        assert_eq!(
1✔
1425
            vres[0],
1✔
1426
            TileInformation::new(
1✔
1427
                [-2, -3].into(),
1✔
1428
                tile_size_in_pixels.into(),
1✔
1429
                central_geo_transform,
1✔
1430
            )
1✔
1431
        );
1✔
1432
        assert_eq!(
1✔
1433
            vres[1],
1✔
1434
            TileInformation::new(
1✔
1435
                [-2, -2].into(),
1✔
1436
                tile_size_in_pixels.into(),
1✔
1437
                central_geo_transform,
1✔
1438
            )
1✔
1439
        );
1✔
1440
        assert_eq!(
1✔
1441
            vres[12],
1✔
1442
            TileInformation::new(
1✔
1443
                [0, -3].into(),
1✔
1444
                tile_size_in_pixels.into(),
1✔
1445
                central_geo_transform,
1✔
1446
            )
1✔
1447
        );
1✔
1448
        assert_eq!(
1✔
1449
            vres[23],
1✔
1450
            TileInformation::new(
1✔
1451
                [1, 2].into(),
1✔
1452
                tile_size_in_pixels.into(),
1✔
1453
                central_geo_transform,
1✔
1454
            )
1✔
1455
        );
1✔
1456
    }
1✔
1457

1458
    #[test]
1✔
1459
    fn replace_time_placeholder() {
1✔
1460
        let params = GdalDatasetParameters {
1✔
1461
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1462
            rasterband_channel: 0,
1✔
1463
            geo_transform: TestDefault::test_default(),
1✔
1464
            width: 360,
1✔
1465
            height: 180,
1✔
1466
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1467
            no_data_value: Some(0.),
1✔
1468
            properties_mapping: None,
1✔
1469
            gdal_open_options: None,
1✔
1470
            gdal_config_options: None,
1✔
1471
            allow_alphaband_as_mask: true,
1✔
1472
            retry: None,
1✔
1473
        };
1✔
1474
        let replaced = params
1✔
1475
            .replace_time_placeholders(
1✔
1476
                &hashmap! {
1✔
1477
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1478
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1479
                        reference: TimeReference::Start,
1✔
1480
                    },
1✔
1481
                },
1✔
1482
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1483
            )
1✔
1484
            .unwrap();
1✔
1485
        assert_eq!(
1✔
1486
            replaced.file_path.to_string_lossy(),
1✔
1487
            "/foo/bar_022000000.tiff".to_string()
1✔
1488
        );
1✔
1489
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1490
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1491
        assert_eq!(params.width, replaced.width);
1✔
1492
        assert_eq!(params.height, replaced.height);
1✔
1493
        assert_eq!(
1✔
1494
            params.file_not_found_handling,
1✔
1495
            replaced.file_not_found_handling
1✔
1496
        );
1✔
1497
    }
1✔
1498

1499
    #[test]
1✔
1500
    fn test_load_tile_data() {
1✔
1501
        let output_shape: GridShape2D = [8, 8].into();
1✔
1502
        let output_bounds =
1✔
1503
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1504

1✔
1505
        let RasterTile2D {
1✔
1506
            global_geo_transform: _,
1✔
1507
            grid_array: grid,
1✔
1508
            tile_position: _,
1✔
1509
            band: _,
1✔
1510
            time: _,
1✔
1511
            properties,
1✔
1512
            cache_hint: _,
1✔
1513
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1514

1✔
1515
        assert!(!grid.is_empty());
1✔
1516

1517
        let grid = grid.into_materialized_masked_grid();
1✔
1518

1✔
1519
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1520
        assert_eq!(
1✔
1521
            grid.inner_grid.data,
1✔
1522
            &[
1✔
1523
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1524
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1525
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1526
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1527
            ]
1✔
1528
        );
1✔
1529

1530
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1531
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1532

1533
        assert!((properties.scale_option()).is_none());
1✔
1534
        assert!(properties.offset_option().is_none());
1✔
1535
        assert_eq!(
1✔
1536
            properties.get_property(&RasterPropertiesKey {
1✔
1537
                key: "AREA_OR_POINT".to_string(),
1✔
1538
                domain: None,
1✔
1539
            }),
1✔
1540
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1541
        );
1✔
1542
        assert_eq!(
1✔
1543
            properties.get_property(&RasterPropertiesKey {
1✔
1544
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1545
                key: "COMPRESSION".to_string(),
1✔
1546
            }),
1✔
1547
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1548
        );
1✔
1549
    }
1✔
1550

1551
    #[test]
1✔
1552
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1553
        let output_shape: GridShape2D = [8, 8].into();
1✔
1554
        // shift world bbox one pixel up and to the left
1✔
1555
        let (x_size, y_size) = (45., 22.5);
1✔
1556
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1557
            (-180. - x_size, 90. + y_size).into(),
1✔
1558
            (180. - x_size, -90. + y_size).into(),
1✔
1559
        );
1✔
1560

1✔
1561
        let RasterTile2D {
1✔
1562
            global_geo_transform: _,
1✔
1563
            grid_array: grid,
1✔
1564
            tile_position: _,
1✔
1565
            band: _,
1✔
1566
            time: _,
1✔
1567
            properties: _,
1✔
1568
            cache_hint: _,
1✔
1569
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1570

1✔
1571
        assert!(!grid.is_empty());
1✔
1572

1573
        let x = grid.into_materialized_masked_grid();
1✔
1574

1✔
1575
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1576
        assert_eq!(
1✔
1577
            x.inner_grid.data,
1✔
1578
            &[
1✔
1579
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1580
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1581
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1582
                255, 255, 255, 255, 255, 255
1✔
1583
            ]
1✔
1584
        );
1✔
1585
    }
1✔
1586

1587
    #[test]
1✔
1588
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1589
        let output_shape: GridShape2D = [8, 8].into();
1✔
1590
        // shift world bbox one pixel up and to the left
1✔
1591
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1592
        let output_bounds = SpatialPartition2D::new(
1✔
1593
            (-116.22222, 66.66666).into(),
1✔
1594
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1595
        )
1✔
1596
        .unwrap();
1✔
1597

1✔
1598
        let RasterTile2D {
1✔
1599
            global_geo_transform: _,
1✔
1600
            grid_array: grid,
1✔
1601
            tile_position: _,
1✔
1602
            band: _,
1✔
1603
            time: _,
1✔
1604
            properties: _,
1✔
1605
            cache_hint: _,
1✔
1606
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1607

1✔
1608
        assert!(!grid.is_empty());
1✔
1609

1610
        let x = grid.into_materialized_masked_grid();
1✔
1611

1✔
1612
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1613
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1614
    }
1✔
1615

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

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

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

1✔
1638
        assert_eq!(c.len(), 4);
1✔
1639

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

1645
        assert_eq!(
1✔
1646
            c[0].tile_information().global_tile_position(),
1✔
1647
            [-1, -1].into()
1✔
1648
        );
1✔
1649

1650
        assert_eq!(
1✔
1651
            c[1].tile_information().global_tile_position(),
1✔
1652
            [-1, 0].into()
1✔
1653
        );
1✔
1654

1655
        assert_eq!(
1✔
1656
            c[2].tile_information().global_tile_position(),
1✔
1657
            [0, -1].into()
1✔
1658
        );
1✔
1659

1660
        assert_eq!(
1✔
1661
            c[3].tile_information().global_tile_position(),
1✔
1662
            [0, 0].into()
1✔
1663
        );
1✔
1664
    }
1665

1666
    #[tokio::test]
1✔
1667
    async fn test_query_multi_time_slices() {
1✔
1668
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1669
        let query_ctx = MockQueryContext::test_default();
1✔
1670
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1671

1✔
1672
        let output_shape: GridShape2D = [256, 256].into();
1✔
1673
        let output_bounds =
1✔
1674
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1675
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-01
1✔
1676

1677
        let c = query_gdal_source(
1✔
1678
            &exe_ctx,
1✔
1679
            &query_ctx,
1✔
1680
            id,
1✔
1681
            output_shape,
1✔
1682
            output_bounds,
1✔
1683
            time_interval,
1✔
1684
        )
1✔
1685
        .await;
9✔
1686
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1687

1✔
1688
        assert_eq!(c.len(), 8);
1✔
1689

1690
        assert_eq!(
1✔
1691
            c[0].time,
1✔
1692
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1693
        );
1✔
1694

1695
        assert_eq!(
1✔
1696
            c[5].time,
1✔
1697
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1698
        );
1✔
1699
    }
1700

1701
    #[tokio::test]
1✔
1702
    async fn test_query_before_data() {
1✔
1703
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1704
        let query_ctx = MockQueryContext::test_default();
1✔
1705
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1706

1✔
1707
        let output_shape: GridShape2D = [256, 256].into();
1✔
1708
        let output_bounds =
1✔
1709
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1710
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1711

1712
        let c = query_gdal_source(
1✔
1713
            &exe_ctx,
1✔
1714
            &query_ctx,
1✔
1715
            id,
1✔
1716
            output_shape,
1✔
1717
            output_bounds,
1✔
1718
            time_interval,
1✔
1719
        )
1✔
1720
        .await;
×
1721
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1722

1✔
1723
        assert_eq!(c.len(), 4);
1✔
1724

1725
        assert_eq!(
1✔
1726
            c[0].time,
1✔
1727
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1728
        );
1✔
1729
    }
1730

1731
    #[tokio::test]
1✔
1732
    async fn test_query_after_data() {
1✔
1733
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1734
        let query_ctx = MockQueryContext::test_default();
1✔
1735
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1736

1✔
1737
        let output_shape: GridShape2D = [256, 256].into();
1✔
1738
        let output_bounds =
1✔
1739
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1740
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1741

1742
        let c = query_gdal_source(
1✔
1743
            &exe_ctx,
1✔
1744
            &query_ctx,
1✔
1745
            id,
1✔
1746
            output_shape,
1✔
1747
            output_bounds,
1✔
1748
            time_interval,
1✔
1749
        )
1✔
1750
        .await;
×
1751
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1752

1✔
1753
        assert_eq!(c.len(), 4);
1✔
1754

1755
        assert_eq!(
1✔
1756
            c[0].time,
1✔
1757
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1758
        );
1✔
1759
    }
1760

1761
    #[tokio::test]
1✔
1762
    async fn test_nodata() {
1✔
1763
        hide_gdal_errors();
1✔
1764

1✔
1765
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1766
        let query_ctx = MockQueryContext::test_default();
1✔
1767
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1768

1✔
1769
        let output_shape: GridShape2D = [256, 256].into();
1✔
1770
        let output_bounds =
1✔
1771
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1772
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1773

1774
        let c = query_gdal_source(
1✔
1775
            &exe_ctx,
1✔
1776
            &query_ctx,
1✔
1777
            id,
1✔
1778
            output_shape,
1✔
1779
            output_bounds,
1✔
1780
            time_interval,
1✔
1781
        )
1✔
1782
        .await;
×
1783
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1784

1✔
1785
        assert_eq!(c.len(), 4);
1✔
1786

1787
        let tile_1 = &c[0];
1✔
1788

1✔
1789
        assert_eq!(
1✔
1790
            tile_1.time,
1✔
1791
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1792
        );
1✔
1793

1794
        assert!(tile_1.is_empty());
1✔
1795
    }
1796

1797
    #[tokio::test]
1✔
1798
    async fn timestep_without_params() {
1✔
1799
        let output_bounds =
1✔
1800
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1801
        let output_shape: GridShape2D = [256, 256].into();
1✔
1802

1✔
1803
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1804
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1805
        let params = None;
1✔
1806

1807
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
1808
            params,
1✔
1809
            tile_info,
1✔
1810
            time_interval,
1✔
1811
            CacheHint::default(),
1✔
1812
        )
1✔
1813
        .await;
×
1814

1815
        assert!(tile.is_ok());
1✔
1816

1817
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1818
            time_interval,
1✔
1819
            tile_info,
1✔
1820
            0,
1✔
1821
            EmptyGrid2D::new(output_shape).into(),
1✔
1822
            CacheHint::default(),
1✔
1823
        );
1✔
1824

1✔
1825
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
1826
    }
1827

1828
    #[test]
1✔
1829
    fn it_reverts_config_options() {
1✔
1830
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1831

1✔
1832
        {
1✔
1833
            let _config =
1✔
1834
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1835

1✔
1836
            assert_eq!(
1✔
1837
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1838
                "bar".to_owned()
1✔
1839
            );
1✔
1840
        }
1841

1842
        assert_eq!(
1✔
1843
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1844
            String::new()
1✔
1845
        );
1✔
1846
    }
1✔
1847

1848
    #[test]
1✔
1849
    #[allow(clippy::too_many_lines)]
1850
    fn deserialize_dataset_parameters() {
1✔
1851
        let dataset_parameters = GdalDatasetParameters {
1✔
1852
            file_path: "path-to-data.tiff".into(),
1✔
1853
            rasterband_channel: 1,
1✔
1854
            geo_transform: GdalDatasetGeoTransform {
1✔
1855
                origin_coordinate: (-180., 90.).into(),
1✔
1856
                x_pixel_size: 0.1,
1✔
1857
                y_pixel_size: -0.1,
1✔
1858
            },
1✔
1859
            width: 3600,
1✔
1860
            height: 1800,
1✔
1861
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1862
            no_data_value: Some(f64::NAN),
1✔
1863
            properties_mapping: Some(vec![
1✔
1864
                GdalMetadataMapping {
1✔
1865
                    source_key: RasterPropertiesKey {
1✔
1866
                        domain: None,
1✔
1867
                        key: "AREA_OR_POINT".to_string(),
1✔
1868
                    },
1✔
1869
                    target_type: RasterPropertiesEntryType::String,
1✔
1870
                    target_key: RasterPropertiesKey {
1✔
1871
                        domain: None,
1✔
1872
                        key: "AREA_OR_POINT".to_string(),
1✔
1873
                    },
1✔
1874
                },
1✔
1875
                GdalMetadataMapping {
1✔
1876
                    source_key: RasterPropertiesKey {
1✔
1877
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
1878
                        key: "COMPRESSION".to_string(),
1✔
1879
                    },
1✔
1880
                    target_type: RasterPropertiesEntryType::String,
1✔
1881
                    target_key: RasterPropertiesKey {
1✔
1882
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1883
                        key: "COMPRESSION".to_string(),
1✔
1884
                    },
1✔
1885
                },
1✔
1886
            ]),
1✔
1887
            gdal_open_options: None,
1✔
1888
            gdal_config_options: None,
1✔
1889
            allow_alphaband_as_mask: true,
1✔
1890
            retry: None,
1✔
1891
        };
1✔
1892

1✔
1893
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1894

1✔
1895
        assert_eq!(
1✔
1896
            dataset_parameters_json,
1✔
1897
            serde_json::json!({
1✔
1898
                "filePath": "path-to-data.tiff",
1✔
1899
                "rasterbandChannel": 1,
1✔
1900
                "geoTransform": {
1✔
1901
                    "originCoordinate": {
1✔
1902
                        "x": -180.,
1✔
1903
                        "y": 90.
1✔
1904
                    },
1✔
1905
                    "xPixelSize": 0.1,
1✔
1906
                    "yPixelSize": -0.1
1✔
1907
                },
1✔
1908
                "width": 3600,
1✔
1909
                "height": 1800,
1✔
1910
                "fileNotFoundHandling": "NoData",
1✔
1911
                "noDataValue": "nan",
1✔
1912
                "propertiesMapping": [{
1✔
1913
                        "source_key": {
1✔
1914
                            "domain": null,
1✔
1915
                            "key": "AREA_OR_POINT"
1✔
1916
                        },
1✔
1917
                        "target_key": {
1✔
1918
                            "domain": null,
1✔
1919
                            "key": "AREA_OR_POINT"
1✔
1920
                        },
1✔
1921
                        "target_type": "String"
1✔
1922
                    },
1✔
1923
                    {
1✔
1924
                        "source_key": {
1✔
1925
                            "domain": "IMAGE_STRUCTURE",
1✔
1926
                            "key": "COMPRESSION"
1✔
1927
                        },
1✔
1928
                        "target_key": {
1✔
1929
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1930
                            "key": "COMPRESSION"
1✔
1931
                        },
1✔
1932
                        "target_type": "String"
1✔
1933
                    }
1✔
1934
                ],
1✔
1935
                "gdalOpenOptions": null,
1✔
1936
                "gdalConfigOptions": null,
1✔
1937
                "allowAlphabandAsMask": true,
1✔
1938
                "retry": null,
1✔
1939
            })
1✔
1940
        );
1✔
1941

1942
        let deserialized_parameters =
1✔
1943
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1944

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

1✔
1947
        assert_eq!(
1✔
1948
            deserialized_parameters.file_path,
1✔
1949
            dataset_parameters.file_path,
1✔
1950
        );
1✔
1951
        assert_eq!(
1✔
1952
            deserialized_parameters.rasterband_channel,
1✔
1953
            dataset_parameters.rasterband_channel,
1✔
1954
        );
1✔
1955
        assert_eq!(
1✔
1956
            deserialized_parameters.geo_transform,
1✔
1957
            dataset_parameters.geo_transform,
1✔
1958
        );
1✔
1959
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
1960
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
1961
        assert_eq!(
1✔
1962
            deserialized_parameters.file_not_found_handling,
1✔
1963
            dataset_parameters.file_not_found_handling,
1✔
1964
        );
1✔
1965
        assert!(
1✔
1966
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
1967
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
1968
        );
1969
        assert_eq!(
1✔
1970
            deserialized_parameters.properties_mapping,
1✔
1971
            dataset_parameters.properties_mapping,
1✔
1972
        );
1✔
1973
        assert_eq!(
1✔
1974
            deserialized_parameters.gdal_open_options,
1✔
1975
            dataset_parameters.gdal_open_options,
1✔
1976
        );
1✔
1977
        assert_eq!(
1✔
1978
            deserialized_parameters.gdal_config_options,
1✔
1979
            dataset_parameters.gdal_config_options,
1✔
1980
        );
1✔
1981
    }
1✔
1982

1983
    #[test]
1✔
1984
    fn gdal_geotransform_to_bounds_neg_y_0() {
1✔
1985
        let gt = GdalDatasetGeoTransform {
1✔
1986
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1987
            x_pixel_size: 1.,
1✔
1988
            y_pixel_size: -1.,
1✔
1989
        };
1✔
1990

1✔
1991
        let sb = gt.spatial_partition(10, 10);
1✔
1992

1✔
1993
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
1994
            .unwrap();
1✔
1995

1✔
1996
        assert_eq!(sb, exp);
1✔
1997
    }
1✔
1998

1999
    #[test]
1✔
2000
    fn gdal_geotransform_to_bounds_neg_y_5() {
1✔
2001
        let gt = GdalDatasetGeoTransform {
1✔
2002
            origin_coordinate: Coordinate2D::new(5., 5.),
1✔
2003
            x_pixel_size: 0.5,
1✔
2004
            y_pixel_size: -0.5,
1✔
2005
        };
1✔
2006

1✔
2007
        let sb = gt.spatial_partition(10, 10);
1✔
2008

1✔
2009
        let exp =
1✔
2010
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1✔
2011

1✔
2012
        assert_eq!(sb, exp);
1✔
2013
    }
1✔
2014

2015
    #[test]
1✔
2016
    fn gdal_geotransform_to_bounds_pos_y_0() {
1✔
2017
        let gt = GdalDatasetGeoTransform {
1✔
2018
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2019
            x_pixel_size: 1.,
1✔
2020
            y_pixel_size: 1.,
1✔
2021
        };
1✔
2022

1✔
2023
        let sb = gt.spatial_partition(10, 10);
1✔
2024

1✔
2025
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2026
            .unwrap();
1✔
2027

1✔
2028
        assert_eq!(sb, exp);
1✔
2029
    }
1✔
2030

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

1✔
2039
        let sb = gt.spatial_partition(10, 10);
1✔
2040

1✔
2041
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
2042
            .unwrap();
1✔
2043

1✔
2044
        assert_eq!(sb, exp);
1✔
2045
    }
1✔
2046

2047
    #[test]
1✔
2048
    fn gdal_read_window_data_origin_upper_left() {
1✔
2049
        let gt = GdalDatasetGeoTransform {
1✔
2050
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
2051
            x_pixel_size: 0.5,
1✔
2052
            y_pixel_size: -0.5,
1✔
2053
        };
1✔
2054

1✔
2055
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
2056
            .unwrap();
1✔
2057

1✔
2058
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2059

1✔
2060
        let exp = GdalReadWindow {
1✔
2061
            read_size_x: 4,
1✔
2062
            read_size_y: 6,
1✔
2063
            read_start_x: 6,
1✔
2064
            read_start_y: 4,
1✔
2065
        };
1✔
2066

1✔
2067
        assert_eq!(rw, exp);
1✔
2068
    }
1✔
2069

2070
    #[test]
1✔
2071
    fn gdal_read_window_data_origin_lower_left() {
1✔
2072
        let gt = GdalDatasetGeoTransform {
1✔
2073
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2074
            x_pixel_size: 1.,
1✔
2075
            y_pixel_size: 1.,
1✔
2076
        };
1✔
2077

1✔
2078
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2079
            .unwrap();
1✔
2080

1✔
2081
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2082

1✔
2083
        let exp = GdalReadWindow {
1✔
2084
            read_size_x: 10,
1✔
2085
            read_size_y: 10,
1✔
2086
            read_start_x: 0,
1✔
2087
            read_start_y: 0,
1✔
2088
        };
1✔
2089

1✔
2090
        assert_eq!(rw, exp);
1✔
2091
    }
1✔
2092

2093
    #[test]
1✔
2094
    fn read_up_side_down_raster() {
1✔
2095
        let output_shape: GridShape2D = [8, 8].into();
1✔
2096
        let output_bounds =
1✔
2097
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2098

1✔
2099
        let up_side_down_params = GdalDatasetParameters {
1✔
2100
            file_path: test_data!(
1✔
2101
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2102
            )
1✔
2103
            .into(),
1✔
2104
            rasterband_channel: 1,
1✔
2105
            geo_transform: GdalDatasetGeoTransform {
1✔
2106
                origin_coordinate: (-180., -90.).into(),
1✔
2107
                x_pixel_size: 0.1,
1✔
2108
                y_pixel_size: 0.1,
1✔
2109
            },
1✔
2110
            width: 3600,
1✔
2111
            height: 1800,
1✔
2112
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2113
            no_data_value: Some(0.),
1✔
2114
            properties_mapping: Some(vec![
1✔
2115
                GdalMetadataMapping {
1✔
2116
                    source_key: RasterPropertiesKey {
1✔
2117
                        domain: None,
1✔
2118
                        key: "AREA_OR_POINT".to_string(),
1✔
2119
                    },
1✔
2120
                    target_type: RasterPropertiesEntryType::String,
1✔
2121
                    target_key: RasterPropertiesKey {
1✔
2122
                        domain: None,
1✔
2123
                        key: "AREA_OR_POINT".to_string(),
1✔
2124
                    },
1✔
2125
                },
1✔
2126
                GdalMetadataMapping {
1✔
2127
                    source_key: RasterPropertiesKey {
1✔
2128
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2129
                        key: "COMPRESSION".to_string(),
1✔
2130
                    },
1✔
2131
                    target_type: RasterPropertiesEntryType::String,
1✔
2132
                    target_key: RasterPropertiesKey {
1✔
2133
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2134
                        key: "COMPRESSION".to_string(),
1✔
2135
                    },
1✔
2136
                },
1✔
2137
            ]),
1✔
2138
            gdal_open_options: None,
1✔
2139
            gdal_config_options: None,
1✔
2140
            allow_alphaband_as_mask: true,
1✔
2141
            retry: None,
1✔
2142
        };
1✔
2143

1✔
2144
        let tile_information =
1✔
2145
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2146

1✔
2147
        let RasterTile2D {
1✔
2148
            global_geo_transform: _,
1✔
2149
            grid_array: grid,
1✔
2150
            tile_position: _,
1✔
2151
            band: _,
1✔
2152
            time: _,
1✔
2153
            properties,
1✔
2154
            cache_hint: _,
1✔
2155
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2156
            &up_side_down_params,
1✔
2157
            tile_information,
1✔
2158
            TimeInterval::default(),
1✔
2159
            CacheHint::default(),
1✔
2160
        )
1✔
2161
        .unwrap();
1✔
2162

1✔
2163
        assert!(!grid.is_empty());
1✔
2164

2165
        let grid = grid.into_materialized_masked_grid();
1✔
2166

1✔
2167
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2168
        assert_eq!(
1✔
2169
            grid.inner_grid.data,
1✔
2170
            &[
1✔
2171
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2172
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2173
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2174
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2175
            ]
1✔
2176
        );
1✔
2177

2178
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2179
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2180

2181
        assert!(properties.offset_option().is_none());
1✔
2182
        assert!(properties.scale_option().is_none());
1✔
2183
    }
1✔
2184

2185
    #[test]
1✔
2186
    fn read_raster_and_offset_scale() {
1✔
2187
        let output_shape: GridShape2D = [8, 8].into();
1✔
2188
        let output_bounds =
1✔
2189
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2190

1✔
2191
        let up_side_down_params = GdalDatasetParameters {
1✔
2192
            file_path: test_data!(
1✔
2193
                "raster/modis_ndvi/with_offset_scale/MOD13A2_M_NDVI_2014-01-01.TIFF"
1✔
2194
            )
1✔
2195
            .into(),
1✔
2196
            rasterband_channel: 1,
1✔
2197
            geo_transform: GdalDatasetGeoTransform {
1✔
2198
                origin_coordinate: (-180., -90.).into(),
1✔
2199
                x_pixel_size: 0.1,
1✔
2200
                y_pixel_size: 0.1,
1✔
2201
            },
1✔
2202
            width: 3600,
1✔
2203
            height: 1800,
1✔
2204
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2205
            no_data_value: Some(0.),
1✔
2206
            properties_mapping: None,
1✔
2207
            gdal_open_options: None,
1✔
2208
            gdal_config_options: None,
1✔
2209
            allow_alphaband_as_mask: true,
1✔
2210
            retry: None,
1✔
2211
        };
1✔
2212

1✔
2213
        let tile_information =
1✔
2214
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2215

1✔
2216
        let RasterTile2D {
1✔
2217
            global_geo_transform: _,
1✔
2218
            grid_array: grid,
1✔
2219
            tile_position: _,
1✔
2220
            band: _,
1✔
2221
            time: _,
1✔
2222
            properties,
1✔
2223
            cache_hint: _,
1✔
2224
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2225
            &up_side_down_params,
1✔
2226
            tile_information,
1✔
2227
            TimeInterval::default(),
1✔
2228
            CacheHint::default(),
1✔
2229
        )
1✔
2230
        .unwrap();
1✔
2231

1✔
2232
        assert!(!grid.is_empty());
1✔
2233

2234
        let grid = grid.into_materialized_masked_grid();
1✔
2235

1✔
2236
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2237
        assert_eq!(
1✔
2238
            grid.inner_grid.data,
1✔
2239
            &[
1✔
2240
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2241
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2242
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2243
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2244
            ]
1✔
2245
        );
1✔
2246

2247
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2248
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2249

2250
        assert_eq!(properties.offset_option(), Some(37.));
1✔
2251
        assert_eq!(properties.scale_option(), Some(3.7));
1✔
2252

2253
        assert!(approx_eq!(f64, properties.offset(), 37.));
1✔
2254
        assert!(approx_eq!(f64, properties.scale(), 3.7));
1✔
2255
    }
1✔
2256

2257
    #[test]
1✔
2258
    #[allow(clippy::too_many_lines)]
2259
    fn it_creates_no_data_only_for_missing_files() {
1✔
2260
        hide_gdal_errors();
1✔
2261

1✔
2262
        let ds = GdalDatasetParameters {
1✔
2263
            file_path: "nonexisting_file.tif".into(),
1✔
2264
            rasterband_channel: 1,
1✔
2265
            geo_transform: TestDefault::test_default(),
1✔
2266
            width: 100,
1✔
2267
            height: 100,
1✔
2268
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2269
            no_data_value: None,
1✔
2270
            properties_mapping: None,
1✔
2271
            gdal_open_options: None,
1✔
2272
            gdal_config_options: None,
1✔
2273
            allow_alphaband_as_mask: true,
1✔
2274
            retry: None,
1✔
2275
        };
1✔
2276

1✔
2277
        let tile_info = TileInformation {
1✔
2278
            tile_size_in_pixels: [100, 100].into(),
1✔
2279
            global_tile_position: [0, 0].into(),
1✔
2280
            global_geo_transform: TestDefault::test_default(),
1✔
2281
        };
1✔
2282

1✔
2283
        let tile_time = TimeInterval::default();
1✔
2284

1✔
2285
        // file doesn't exist => no data
1✔
2286
        let result =
1✔
2287
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2288
                .unwrap();
1✔
2289
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2290

2291
        let ds = GdalDatasetParameters {
1✔
2292
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2293
            rasterband_channel: 100, // invalid channel
1✔
2294
            geo_transform: TestDefault::test_default(),
1✔
2295
            width: 100,
1✔
2296
            height: 100,
1✔
2297
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2298
            no_data_value: None,
1✔
2299
            properties_mapping: None,
1✔
2300
            gdal_open_options: None,
1✔
2301
            gdal_config_options: None,
1✔
2302
            allow_alphaband_as_mask: true,
1✔
2303
            retry: None,
1✔
2304
        };
1✔
2305

1✔
2306
        // invalid channel => error
1✔
2307
        let result =
1✔
2308
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2309
        assert!(result.is_err());
1✔
2310

2311
        let server = Server::run();
1✔
2312

1✔
2313
        server.expect(
1✔
2314
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2315
                .times(1)
1✔
2316
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2317
        );
1✔
2318

1✔
2319
        server.expect(
1✔
2320
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2321
                .times(1)
1✔
2322
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2323
        );
1✔
2324

1✔
2325
        let ds = GdalDatasetParameters {
1✔
2326
            file_path: format!("/vsicurl/{}", server.url_str("/non_existing.tif")).into(),
1✔
2327
            rasterband_channel: 1,
1✔
2328
            geo_transform: TestDefault::test_default(),
1✔
2329
            width: 100,
1✔
2330
            height: 100,
1✔
2331
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2332
            no_data_value: None,
1✔
2333
            properties_mapping: None,
1✔
2334
            gdal_open_options: None,
1✔
2335
            gdal_config_options: Some(vec![
1✔
2336
                (
1✔
2337
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2338
                    ".tif".to_owned(),
1✔
2339
                ),
1✔
2340
                (
1✔
2341
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2342
                    "EMPTY_DIR".to_owned(),
1✔
2343
                ),
1✔
2344
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2345
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2346
            ]),
1✔
2347
            allow_alphaband_as_mask: true,
1✔
2348
            retry: None,
1✔
2349
        };
1✔
2350

1✔
2351
        // 404 => no data
1✔
2352
        let result =
1✔
2353
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2354
                .unwrap();
1✔
2355
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2356

2357
        let ds = GdalDatasetParameters {
1✔
2358
            file_path: format!("/vsicurl/{}", server.url_str("/internal_error.tif")).into(),
1✔
2359
            rasterband_channel: 1,
1✔
2360
            geo_transform: TestDefault::test_default(),
1✔
2361
            width: 100,
1✔
2362
            height: 100,
1✔
2363
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2364
            no_data_value: None,
1✔
2365
            properties_mapping: None,
1✔
2366
            gdal_open_options: None,
1✔
2367
            gdal_config_options: Some(vec![
1✔
2368
                (
1✔
2369
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2370
                    ".tif".to_owned(),
1✔
2371
                ),
1✔
2372
                (
1✔
2373
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2374
                    "EMPTY_DIR".to_owned(),
1✔
2375
                ),
1✔
2376
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2377
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2378
            ]),
1✔
2379
            allow_alphaband_as_mask: true,
1✔
2380
            retry: None,
1✔
2381
        };
1✔
2382

1✔
2383
        // 500 => error
1✔
2384
        let result =
1✔
2385
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2386
        assert!(result.is_err());
1✔
2387
    }
1✔
2388

2389
    #[test]
1✔
2390
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2391
        hide_gdal_errors();
1✔
2392

1✔
2393
        let server = Server::run();
1✔
2394

1✔
2395
        server.expect(
1✔
2396
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2397
                .times(2)
1✔
2398
                .respond_with(responders::cycle![
1✔
2399
                    // first generic error
1✔
2400
                    responders::status_code(500),
1✔
2401
                    // then 404 file not found
1✔
2402
                    responders::status_code(404)
1✔
2403
                ]),
1✔
2404
        );
1✔
2405

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

1✔
2408
        let options = Some(vec![
1✔
2409
            (
1✔
2410
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2411
                ".tif".to_owned(),
1✔
2412
            ),
1✔
2413
            (
1✔
2414
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2415
                "EMPTY_DIR".to_owned(),
1✔
2416
            ),
1✔
2417
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2418
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2419
        ]);
1✔
2420

1✔
2421
        let _thread_local_configs = options
1✔
2422
            .as_ref()
1✔
2423
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2424

1✔
2425
        // first fail
1✔
2426
        let result = gdal_open_dataset_ex(
1✔
2427
            file_path.as_path(),
1✔
2428
            DatasetOptions {
1✔
2429
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2430
                ..DatasetOptions::default()
1✔
2431
            },
1✔
2432
        );
1✔
2433

1✔
2434
        // it failed, but not with file not found
1✔
2435
        assert!(result.is_err());
1✔
2436
        if let Err(error) = result {
1✔
2437
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2438
        }
×
2439

2440
        // second fail doesn't even try, so still not "file not found", even though it should be now
2441
        let result = gdal_open_dataset_ex(
1✔
2442
            file_path.as_path(),
1✔
2443
            DatasetOptions {
1✔
2444
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2445
                ..DatasetOptions::default()
1✔
2446
            },
1✔
2447
        );
1✔
2448

1✔
2449
        assert!(result.is_err());
1✔
2450
        if let Err(error) = result {
1✔
2451
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2452
        }
×
2453

2454
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2455

1✔
2456
        // after clearing the cache, it tries again
1✔
2457
        let result = gdal_open_dataset_ex(
1✔
2458
            file_path.as_path(),
1✔
2459
            DatasetOptions {
1✔
2460
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2461
                ..DatasetOptions::default()
1✔
2462
            },
1✔
2463
        );
1✔
2464

1✔
2465
        // now we get the file not found error
1✔
2466
        assert!(result.is_err());
1✔
2467
        if let Err(error) = result {
1✔
2468
            assert!(error_is_gdal_file_not_found(&error));
1✔
2469
        }
×
2470
    }
1✔
2471

2472
    #[tokio::test]
1✔
2473
    async fn it_attaches_cache_hint() {
1✔
2474
        let output_bounds =
1✔
2475
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
2476
        let output_shape: GridShape2D = [256, 256].into();
1✔
2477

1✔
2478
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2479
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
2480
        let params = None;
1✔
2481

2482
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
2483
            params,
1✔
2484
            tile_info,
1✔
2485
            time_interval,
1✔
2486
            CacheHint::seconds(1234),
1✔
2487
        )
1✔
2488
        .await;
×
2489

2490
        assert!(tile.is_ok());
1✔
2491

2492
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
2493
            time_interval,
1✔
2494
            tile_info,
1✔
2495
            0,
1✔
2496
            EmptyGrid2D::new(output_shape).into(),
1✔
2497
            CacheHint::seconds(1234),
1✔
2498
        );
1✔
2499

1✔
2500
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
2501
    }
2502
}
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