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

geo-engine / geoengine / 12985123830

27 Jan 2025 08:45AM UTC coverage: 90.03% (-0.07%) from 90.097%
12985123830

Pull #1011

github

web-flow
Merge e2b187685 into ef1edce10
Pull Request #1011: update to rust 1.84

22 of 27 new or added lines in 13 files covered. (81.48%)

103 existing lines in 42 files now uncovered.

125586 of 139494 relevant lines covered (90.03%)

57703.25 hits per line

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

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

66
mod db_types;
67
mod error;
68
mod loading_info;
69

70
static GDAL_RETRY_INITIAL_BACKOFF_MS: u64 = 1000;
71
static GDAL_RETRY_MAX_BACKOFF_MS: u64 = 60 * 60 * 1000;
72
static GDAL_RETRY_EXPONENTIAL_BACKOFF_FACTOR: f64 = 2.;
73

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

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

111
type GdalMetaData =
112
    Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>>;
113

UNCOV
114
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
×
115
#[serde(rename_all = "camelCase")]
116
pub struct GdalSourceTimePlaceholder {
117
    pub format: DateTimeParseFormat,
118
    pub reference: TimeReference,
119
}
120

UNCOV
121
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
×
122
#[serde(rename_all = "camelCase")]
123
pub enum TimeReference {
124
    Start,
125
    End,
126
}
127

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

153
#[derive(PartialEq, Serialize, Deserialize, Debug, Clone, Copy)]
154
#[serde(rename_all = "camelCase")]
155
pub struct GdalRetryOptions {
156
    pub max_retries: usize,
157
}
158

159
#[derive(Debug, PartialEq, Eq)]
160
struct GdalReadWindow {
161
    start_x: isize, // pixelspace origin
162
    start_y: isize,
163
    size_x: usize, // pixelspace size
164
    size_y: usize,
165
}
166

167
impl GdalReadWindow {
168
    fn gdal_window_start(&self) -> (isize, isize) {
855✔
169
        (self.start_x, self.start_y)
855✔
170
    }
855✔
171

172
    fn gdal_window_size(&self) -> (usize, usize) {
855✔
173
        (self.size_x, self.size_y)
855✔
174
    }
855✔
175
}
176

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

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

196
        // if the y-axis is negative then the origin is on the upper side.
197
        let (upper_y, lower_y) = if self.y_pixel_size.is_sign_negative() {
1,722✔
198
            (self.origin_coordinate.y, opposite_coord_y)
1,719✔
199
        } else {
200
            (opposite_coord_y, self.origin_coordinate.y)
3✔
201
        };
202

203
        let opposite_coord_x = self.origin_coordinate.x + self.x_pixel_size * x_size as f64;
1,722✔
204

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

212
        SpatialPartition2D::new_unchecked(
1,722✔
213
            Coordinate2D::new(left_x, upper_y),
1,722✔
214
            Coordinate2D::new(right_x, lower_y),
1,722✔
215
        )
1,722✔
216
    }
1,722✔
217

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

1,696✔
227
        [grid_y_index, grid_x_index].into()
1,696✔
228
    }
1,696✔
229

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

239
        /*
240
        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:
241

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

253
        However, sometimes the data is stored up-side down. Like this:
254

255
        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.
256

257
        ul                      ur
258
        +_______________________+
259
        |      _                |
260
        |    _|_|  row ...      |
261
        |  _|_|  row 3          |
262
        | |_|  row 2            |
263
        |_______________________|
264
        +                       *
265
        ll                      lr
266

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

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

848✔
286
        let GridIdx([near_idx_y, near_idx_x]) = self.coordinate_to_grid_idx_2d(safe_near_coord);
848✔
287
        let GridIdx([far_idx_y, far_idx_x]) = self.coordinate_to_grid_idx_2d(safe_far_coord);
848✔
288

848✔
289
        debug_assert!(near_idx_x <= far_idx_x);
848✔
290
        debug_assert!(near_idx_y <= far_idx_y);
848✔
291

292
        let read_size_x = (far_idx_x - near_idx_x) as usize + 1;
848✔
293
        let read_size_y = (far_idx_y - near_idx_y) as usize + 1;
848✔
294

848✔
295
        GdalReadWindow {
848✔
296
            start_x: near_idx_x,
848✔
297
            start_y: near_idx_y,
848✔
298
            size_x: read_size_x,
848✔
299
            size_y: read_size_y,
848✔
300
        }
848✔
301
    }
848✔
302
}
303

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

315
impl ApproxEq for GdalDatasetGeoTransform {
316
    type Margin = float_cmp::F64Margin;
317

318
    fn approx_eq<M>(self, other: Self, margin: M) -> bool
846✔
319
    where
846✔
320
        M: Into<Self::Margin>,
846✔
321
    {
846✔
322
        let m = margin.into();
846✔
323
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
846✔
324
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
845✔
325
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
845✔
326
    }
846✔
327
}
328

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

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

339
        Ok(GeoTransform::new(
×
340
            dataset_geo_transform.origin_coordinate,
×
341
            dataset_geo_transform.x_pixel_size,
×
342
            dataset_geo_transform.y_pixel_size,
×
343
        ))
×
344
    }
×
345
}
346

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

357
impl SpatialPartitioned for GdalDatasetParameters {
358
    fn spatial_partition(&self) -> SpatialPartition2D {
872✔
359
        self.geo_transform
872✔
360
            .spatial_partition(self.width, self.height)
872✔
361
    }
872✔
362
}
363

364
impl GridShapeAccess for GdalDatasetParameters {
365
    type ShapeArray = [usize; 2];
366

367
    fn grid_shape_array(&self) -> Self::ShapeArray {
×
368
        [self.height, self.width]
×
369
    }
×
370
}
371

372
/// How to handle file not found errors
UNCOV
373
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, FromSql, ToSql)]
×
374
pub enum FileNotFoundHandling {
375
    NoData, // output tiles filled with nodata
376
    Error,  // return error tile
377
}
378

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

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

115✔
399
            // TODO: use more efficient algorithm for replacing multiple placeholders, e.g. aho-corasick
115✔
400
            file_path = file_path.replace(placeholder, &time_string);
115✔
401
        }
402

403
        Ok(Self {
115✔
404
            file_not_found_handling: self.file_not_found_handling,
115✔
405
            file_path: file_path.into(),
115✔
406
            properties_mapping: self.properties_mapping.clone(),
115✔
407
            gdal_open_options: self.gdal_open_options.clone(),
115✔
408
            gdal_config_options: self.gdal_config_options.clone(),
115✔
409
            ..*self
115✔
410
        })
115✔
411
    }
115✔
412
}
413

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

422
pub struct GdalSourceProcessor<T>
423
where
424
    T: Pixel,
425
{
426
    pub result_descriptor: RasterResultDescriptor,
427
    pub tiling_specification: TilingSpecification,
428
    pub meta_data: GdalMetaData,
429
    pub _phantom_data: PhantomData<T>,
430
}
431

432
struct GdalRasterLoader {}
433

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

839✔
447
        retry(
839✔
448
            dataset_params
839✔
449
                .retry
839✔
450
                .map(|r| r.max_retries)
839✔
451
                .unwrap_or_default(),
839✔
452
            GDAL_RETRY_INITIAL_BACKOFF_MS,
839✔
453
            GDAL_RETRY_EXPONENTIAL_BACKOFF_FACTOR,
839✔
454
            Some(GDAL_RETRY_MAX_BACKOFF_MS),
839✔
455
            move || {
845✔
456
                let ds = dataset_params.clone();
845✔
457
                let file_path = ds.file_path.clone();
845✔
458

459
                async move {
845✔
460
                    let load_tile_result = crate::util::spawn_blocking(move || {
845✔
461
                        Self::load_tile_data(&ds, tile_information, tile_time, cache_hint)
845✔
462
                    })
845✔
463
                    .await
845✔
464
                    .context(crate::error::TokioJoin);
826✔
465

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

475
                            Err(e)
6✔
476
                        }
477
                    }
478
                }
826✔
479
            },
845✔
480
        )
839✔
481
        .await
839✔
482
    }
820✔
483

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

506
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
33✔
507
            }
508

509
            _ => {
510
                debug!(
46✔
511
                    "Skipping tile without GdalDatasetParameters {:?}",
×
512
                    &tile_information
×
513
                );
514

515
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
46✔
516
            }
517
        }
518
    }
899✔
519

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

854✔
531
        debug!(
854✔
532
            "GridOrEmpty2D<{:?}> requested for {:?}.",
×
533
            T::TYPE,
×
534
            &tile_information.spatial_partition()
×
535
        );
536

537
        let options = dataset_params
854✔
538
            .gdal_open_options
854✔
539
            .as_ref()
854✔
540
            .map(|o| o.iter().map(String::as_str).collect::<Vec<_>>());
854✔
541

854✔
542
        // reverts the thread local configs on drop
854✔
543
        let _thread_local_configs = dataset_params
854✔
544
            .gdal_config_options
854✔
545
            .as_ref()
854✔
546
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
854✔
547

854✔
548
        let dataset_result = gdal_open_dataset_ex(
854✔
549
            &dataset_params.file_path,
854✔
550
            DatasetOptions {
854✔
551
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
854✔
552
                open_options: options.as_deref(),
854✔
553
                ..DatasetOptions::default()
854✔
554
            },
854✔
555
        );
854✔
556

557
        if let Err(error) = &dataset_result {
854✔
558
            let is_file_not_found = error_is_gdal_file_not_found(error);
7✔
559

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

847✔
579
        let dataset = dataset_result.expect("checked");
847✔
580

581
        let result_tile = read_raster_tile_with_properties(
847✔
582
            &dataset,
847✔
583
            dataset_params,
847✔
584
            tile_information,
847✔
585
            tile_time,
847✔
586
            cache_hint,
847✔
587
        )?;
847✔
588

589
        let elapsed = start.elapsed();
844✔
590
        debug!("data loaded -> returning data grid, took {:?}", elapsed);
844✔
591

592
        Ok(result_tile)
844✔
593
    }
854✔
594

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

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

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

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

653
impl<T> GdalSourceProcessor<T> where T: gdal::raster::GdalType + Pixel {}
654

655
#[async_trait]
656
impl<P> QueryProcessor for GdalSourceProcessor<P>
657
where
658
    P: Pixel + gdal::raster::GdalType + FromPrimitive,
659
{
660
    type Output = RasterTile2D<P>;
661
    type SpatialBounds = SpatialPartition2D;
662
    type Selection = BandSelection;
663
    type ResultDescription = RasterResultDescriptor;
664

665
    async fn _query<'a>(
666
        &'a self,
667
        query: RasterQueryRectangle,
668
        _ctx: &'a dyn crate::engine::QueryContext,
669
    ) -> Result<BoxStream<Result<Self::Output>>> {
120✔
670
        ensure!(
120✔
671
            query.attributes.as_slice() == [0],
120✔
672
            crate::error::GdalSourceDoesNotSupportQueryingOtherBandsThanTheFirstOneYet
×
673
        );
674

675
        let start = Instant::now();
120✔
676
        debug!(
120✔
677
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
×
678
            P::TYPE,
×
679
            &query
×
680
        );
681

682
        debug!(
120✔
683
            "GdalSourceProcessor<{:?}> meta data loaded, took {:?}.",
×
684
            P::TYPE,
×
685
            start.elapsed()
×
686
        );
687

688
        let spatial_resolution = query.spatial_resolution;
120✔
689

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

703
        let tiling_strategy = self
120✔
704
            .tiling_specification
120✔
705
            .strategy(pixel_size_x, pixel_size_y);
120✔
706

707
        let result_descriptor = self.meta_data.result_descriptor().await?;
120✔
708

709
        let mut empty = false;
120✔
710
        debug!("result descr bbox: {:?}", result_descriptor.bbox);
120✔
711
        debug!("query bbox: {:?}", query.spatial_bounds);
120✔
712

713
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
120✔
714
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
81✔
715
                debug!("query does not intersect spatial data bounds");
5✔
716
                empty = true;
5✔
717
            }
76✔
718
        }
39✔
719

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

730
        let loading_info = if empty {
120✔
731
            // TODO: using this shortcut will insert one no-data element with max time validity. However, this does not honor time intervals of data in other areas!
732
            GdalLoadingInfo::new(
5✔
733
                GdalLoadingInfoTemporalSliceIterator::Static {
5✔
734
                    parts: vec![].into_iter(),
5✔
735
                },
5✔
736
                TimeInstance::MIN,
5✔
737
                TimeInstance::MAX,
5✔
738
            )
5✔
739
        } else {
740
            self.meta_data.loading_info(query.clone()).await?
115✔
741
        };
742

743
        let time_bounds = match (
120✔
744
            loading_info.start_time_of_output_stream,
120✔
745
            loading_info.end_time_of_output_stream,
120✔
746
        ) {
747
            (Some(start), Some(end)) => FillerTimeBounds::new(start, end),
120✔
748
            (None, None) => {
749
                log::warn!("The provider did not provide a time range that covers the query. Falling back to query time range. ");
×
750
                FillerTimeBounds::new(query.time_interval.start(), query.time_interval.end())
×
751
            }
752
            (Some(start), None) => {
×
753
                log::warn!("The provider did only provide a time range start that covers the query. Falling back to query time end. ");
×
754
                FillerTimeBounds::new(start, query.time_interval.end())
×
755
            }
756
            (None, Some(end)) => {
×
757
                log::warn!("The provider did only provide a time range end that covers the query. Falling back to query time start. ");
×
758
                FillerTimeBounds::new(query.time_interval.start(), end)
×
759
            }
760
        };
761

762
        let query_time = query.time_interval;
120✔
763
        let skipping_loading_info = loading_info
120✔
764
            .info
120✔
765
            .filter_ok(move |s: &GdalLoadingInfoTemporalSlice| s.time.intersects(&query_time));
131✔
766

120✔
767
        let source_stream = stream::iter(skipping_loading_info);
120✔
768

120✔
769
        let source_stream = GdalRasterLoader::loading_info_to_tile_stream(
120✔
770
            source_stream,
120✔
771
            query.clone(),
120✔
772
            tiling_strategy,
120✔
773
        );
120✔
774

120✔
775
        // use SparseTilesFillAdapter to fill all the gaps
120✔
776
        let filled_stream = SparseTilesFillAdapter::new(
120✔
777
            source_stream,
120✔
778
            tiling_strategy.tile_grid_box(query.spatial_partition()),
120✔
779
            query.attributes.count(),
120✔
780
            tiling_strategy.geo_transform,
120✔
781
            tiling_strategy.tile_size_in_pixels,
120✔
782
            FillerTileCacheExpirationStrategy::DerivedFromSurroundingTiles,
120✔
783
            query.time_interval,
120✔
784
            time_bounds,
120✔
785
        );
120✔
786
        Ok(filled_stream.boxed())
120✔
787
    }
240✔
788

789
    fn result_descriptor(&self) -> &RasterResultDescriptor {
334✔
790
        &self.result_descriptor
334✔
791
    }
334✔
792
}
793

794
pub type GdalSource = SourceOperator<GdalSourceParameters>;
795

796
impl OperatorName for GdalSource {
797
    const TYPE_NAME: &'static str = "GdalSource";
798
}
799

800
#[typetag::serde]
157✔
801
#[async_trait]
802
impl RasterOperator for GdalSource {
803
    async fn _initialize(
804
        self: Box<Self>,
805
        path: WorkflowOperatorPath,
806
        context: &dyn crate::engine::ExecutionContext,
807
    ) -> Result<Box<dyn InitializedRasterOperator>> {
63✔
808
        let data_id = context.resolve_named_data(&self.params.data).await?;
63✔
809
        let meta_data: GdalMetaData = context.meta_data(&data_id).await?;
61✔
810

811
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
60✔
812
        debug!("GdalSource path: {:?}", path);
60✔
813

814
        let op = InitializedGdalSourceOperator {
60✔
815
            name: CanonicOperatorName::from(&self),
60✔
816
            path,
60✔
817
            data: self.params.data.to_string(),
60✔
818
            result_descriptor: meta_data.result_descriptor().await?,
60✔
819
            meta_data,
60✔
820
            tiling_specification: context.tiling_specification(),
60✔
821
        };
60✔
822

60✔
823
        Ok(op.boxed())
60✔
824
    }
126✔
825

826
    span_fn!(GdalSource);
827
}
828

829
pub struct InitializedGdalSourceOperator {
830
    name: CanonicOperatorName,
831
    path: WorkflowOperatorPath,
832
    data: String,
833
    pub meta_data: GdalMetaData,
834
    pub result_descriptor: RasterResultDescriptor,
835
    pub tiling_specification: TilingSpecification,
836
}
837

838
impl InitializedRasterOperator for InitializedGdalSourceOperator {
839
    fn result_descriptor(&self) -> &RasterResultDescriptor {
97✔
840
        &self.result_descriptor
97✔
841
    }
97✔
842

843
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
51✔
844
        Ok(match self.result_descriptor().data_type {
51✔
845
            RasterDataType::U8 => TypedRasterQueryProcessor::U8(
48✔
846
                GdalSourceProcessor {
48✔
847
                    result_descriptor: self.result_descriptor.clone(),
48✔
848
                    tiling_specification: self.tiling_specification,
48✔
849
                    meta_data: self.meta_data.clone(),
48✔
850
                    _phantom_data: PhantomData,
48✔
851
                }
48✔
852
                .boxed(),
48✔
853
            ),
48✔
854
            RasterDataType::U16 => TypedRasterQueryProcessor::U16(
2✔
855
                GdalSourceProcessor {
2✔
856
                    result_descriptor: self.result_descriptor.clone(),
2✔
857
                    tiling_specification: self.tiling_specification,
2✔
858
                    meta_data: self.meta_data.clone(),
2✔
859
                    _phantom_data: PhantomData,
2✔
860
                }
2✔
861
                .boxed(),
2✔
862
            ),
2✔
863
            RasterDataType::U32 => TypedRasterQueryProcessor::U32(
×
864
                GdalSourceProcessor {
×
865
                    result_descriptor: self.result_descriptor.clone(),
×
866
                    tiling_specification: self.tiling_specification,
×
867
                    meta_data: self.meta_data.clone(),
×
868
                    _phantom_data: PhantomData,
×
869
                }
×
870
                .boxed(),
×
871
            ),
×
872
            RasterDataType::U64 => {
873
                return Err(GdalSourceError::UnsupportedRasterType {
×
874
                    raster_type: RasterDataType::U64,
×
875
                })?
×
876
            }
877
            RasterDataType::I8 => {
878
                return Err(GdalSourceError::UnsupportedRasterType {
×
879
                    raster_type: RasterDataType::I8,
×
880
                })?
×
881
            }
882
            RasterDataType::I16 => TypedRasterQueryProcessor::I16(
1✔
883
                GdalSourceProcessor {
1✔
884
                    result_descriptor: self.result_descriptor.clone(),
1✔
885
                    tiling_specification: self.tiling_specification,
1✔
886
                    meta_data: self.meta_data.clone(),
1✔
887
                    _phantom_data: PhantomData,
1✔
888
                }
1✔
889
                .boxed(),
1✔
890
            ),
1✔
891
            RasterDataType::I32 => TypedRasterQueryProcessor::I32(
×
892
                GdalSourceProcessor {
×
893
                    result_descriptor: self.result_descriptor.clone(),
×
894
                    tiling_specification: self.tiling_specification,
×
895
                    meta_data: self.meta_data.clone(),
×
896
                    _phantom_data: PhantomData,
×
897
                }
×
898
                .boxed(),
×
899
            ),
×
900
            RasterDataType::I64 => {
901
                return Err(GdalSourceError::UnsupportedRasterType {
×
902
                    raster_type: RasterDataType::I64,
×
903
                })?
×
904
            }
905
            RasterDataType::F32 => TypedRasterQueryProcessor::F32(
×
906
                GdalSourceProcessor {
×
907
                    result_descriptor: self.result_descriptor.clone(),
×
908
                    tiling_specification: self.tiling_specification,
×
909
                    meta_data: self.meta_data.clone(),
×
910
                    _phantom_data: PhantomData,
×
911
                }
×
912
                .boxed(),
×
913
            ),
×
914
            RasterDataType::F64 => TypedRasterQueryProcessor::F64(
×
915
                GdalSourceProcessor {
×
916
                    result_descriptor: self.result_descriptor.clone(),
×
917
                    tiling_specification: self.tiling_specification,
×
918
                    meta_data: self.meta_data.clone(),
×
919
                    _phantom_data: PhantomData,
×
920
                }
×
921
                .boxed(),
×
922
            ),
×
923
        })
924
    }
51✔
925

926
    fn canonic_name(&self) -> CanonicOperatorName {
1✔
927
        self.name.clone()
1✔
928
    }
1✔
929

930
    fn name(&self) -> &'static str {
25✔
931
        GdalSource::TYPE_NAME
25✔
932
    }
25✔
933

934
    fn path(&self) -> WorkflowOperatorPath {
25✔
935
        self.path.clone()
25✔
936
    }
25✔
937

938
    fn data(&self) -> Option<String> {
25✔
939
        Some(self.data.clone())
25✔
940
    }
25✔
941
}
942

943
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
944
/// It fails if the tile is not within the dataset.
945
#[allow(clippy::float_cmp)]
946
fn read_grid_from_raster<T, D>(
846✔
947
    rasterband: &GdalRasterBand,
846✔
948
    read_window: &GdalReadWindow,
846✔
949
    out_shape: D,
846✔
950
    dataset_params: &GdalDatasetParameters,
846✔
951
    flip_y_axis: bool,
846✔
952
) -> Result<GridOrEmpty<D, T>>
846✔
953
where
846✔
954
    T: Pixel + GdalType + Default + FromPrimitive,
846✔
955
    D: Clone + GridSize + PartialEq,
846✔
956
{
846✔
957
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
846✔
958

959
    let buffer = rasterband.read_as::<T>(
846✔
960
        read_window.gdal_window_start(), // pixelspace origin
846✔
961
        read_window.gdal_window_size(),  // pixelspace size
846✔
962
        gdal_out_shape,                  // requested raster size
846✔
963
        None,                            // sampling mode
846✔
964
    )?;
846✔
965
    let (_, buffer_data) = buffer.into_shape_and_vec();
844✔
966
    let data_grid = Grid::new(out_shape.clone(), buffer_data)?;
844✔
967

968
    let data_grid = if flip_y_axis {
844✔
969
        data_grid.reversed_y_axis_grid()
1✔
970
    } else {
971
        data_grid
843✔
972
    };
973

974
    let dataset_mask_flags = rasterband.mask_flags()?;
844✔
975

976
    if dataset_mask_flags.is_all_valid() {
844✔
977
        debug!("all pixels are valid --> skip no-data and mask handling.");
16✔
978
        return Ok(MaskedGrid::new_with_data(data_grid).into());
16✔
979
    }
828✔
980

828✔
981
    if dataset_mask_flags.is_nodata() {
828✔
982
        debug!("raster uses a no-data value --> use no-data handling.");
819✔
983
        let no_data_value = dataset_params
819✔
984
            .no_data_value
819✔
985
            .or_else(|| rasterband.no_data_value())
819✔
986
            .and_then(FromPrimitive::from_f64);
819✔
987
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
819✔
988
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
819✔
989
        return Ok(grid_or_empty);
819✔
990
    }
9✔
991

9✔
992
    if dataset_mask_flags.is_alpha() {
9✔
993
        debug!("raster uses alpha band to mask pixels.");
×
994
        if !dataset_params.allow_alphaband_as_mask {
×
995
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
996
        }
×
997
    }
9✔
998

999
    debug!("use mask based no-data handling.");
9✔
1000

1001
    let mask_band = rasterband.open_mask_band()?;
9✔
1002
    let mask_buffer = mask_band.read_as::<u8>(
9✔
1003
        read_window.gdal_window_start(), // pixelspace origin
9✔
1004
        read_window.gdal_window_size(),  // pixelspace size
9✔
1005
        gdal_out_shape,                  // requested raster size
9✔
1006
        None,                            // sampling mode
9✔
1007
    )?;
9✔
1008
    let (_, mask_buffer_data) = mask_buffer.into_shape_and_vec();
9✔
1009
    let mask_grid = Grid::new(out_shape, mask_buffer_data)?.map_elements(|p: u8| p > 0);
360,020✔
1010

1011
    let mask_grid = if flip_y_axis {
9✔
1012
        mask_grid.reversed_y_axis_grid()
×
1013
    } else {
1014
        mask_grid
9✔
1015
    };
1016

1017
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
9✔
1018
    Ok(GridOrEmpty::from(masked_grid))
9✔
1019
}
846✔
1020

1021
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
1022
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
1023
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
1024
fn read_partial_grid_from_raster<T>(
501✔
1025
    rasterband: &GdalRasterBand,
501✔
1026
    gdal_read_window: &GdalReadWindow,
501✔
1027
    out_tile_read_bounds: GridBoundingBox2D,
501✔
1028
    out_tile_shape: GridShape2D,
501✔
1029
    dataset_params: &GdalDatasetParameters,
501✔
1030
    flip_y_axis: bool,
501✔
1031
) -> Result<GridOrEmpty2D<T>>
501✔
1032
where
501✔
1033
    T: Pixel + GdalType + Default + FromPrimitive,
501✔
1034
{
501✔
1035
    let dataset_raster = read_grid_from_raster(
501✔
1036
        rasterband,
501✔
1037
        gdal_read_window,
501✔
1038
        out_tile_read_bounds,
501✔
1039
        dataset_params,
501✔
1040
        flip_y_axis,
501✔
1041
    )?;
501✔
1042

1043
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
499✔
1044
    tile_raster.grid_blit_from(&dataset_raster);
499✔
1045
    Ok(tile_raster)
499✔
1046
}
501✔
1047

1048
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
1049
/// It handles conversion to grid coordinates.
1050
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
1051
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
1052
fn read_grid_and_handle_edges<T>(
846✔
1053
    tile_info: TileInformation,
846✔
1054
    dataset: &GdalDataset,
846✔
1055
    rasterband: &GdalRasterBand,
846✔
1056
    dataset_params: &GdalDatasetParameters,
846✔
1057
) -> Result<GridOrEmpty2D<T>>
846✔
1058
where
846✔
1059
    T: Pixel + GdalType + Default + FromPrimitive,
846✔
1060
{
846✔
1061
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
846✔
1062
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
846✔
1063

846✔
1064
    if !approx_eq!(
846✔
1065
        GdalDatasetGeoTransform,
846✔
1066
        gdal_dataset_geotransform,
846✔
1067
        dataset_params.geo_transform
846✔
1068
    ) {
846✔
1069
        log::warn!(
1✔
1070
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
1071
            dataset_params.geo_transform,
1072
            gdal_dataset_geotransform,
1073
        );
1074
    };
845✔
1075

1076
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
846✔
1077
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
846✔
1078

1079
    let gdal_dataset_bounds =
846✔
1080
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
846✔
1081

846✔
1082
    let output_bounds = tile_info.spatial_partition();
846✔
1083
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
846✔
1084
    let output_shape = tile_info.tile_size_in_pixels();
846✔
1085

1086
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
846✔
1087
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
1088
    };
1089

1090
    let tile_geo_transform = tile_info.tile_geo_transform();
846✔
1091

846✔
1092
    let gdal_read_window =
846✔
1093
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
846✔
1094

846✔
1095
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
846✔
1096
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
846✔
1097

846✔
1098
    if is_y_axis_flipped {
846✔
1099
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1100
    }
845✔
1101

1102
    let result_grid = if dataset_intersection_area == output_bounds {
846✔
1103
        read_grid_from_raster(
345✔
1104
            rasterband,
345✔
1105
            &gdal_read_window,
345✔
1106
            output_shape,
345✔
1107
            dataset_params,
345✔
1108
            is_y_axis_flipped,
345✔
1109
        )?
345✔
1110
    } else {
1111
        let partial_tile_grid_bounds =
501✔
1112
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
501✔
1113

501✔
1114
        read_partial_grid_from_raster(
501✔
1115
            rasterband,
501✔
1116
            &gdal_read_window,
501✔
1117
            partial_tile_grid_bounds,
501✔
1118
            output_shape,
501✔
1119
            dataset_params,
501✔
1120
            is_y_axis_flipped,
501✔
1121
        )?
501✔
1122
    };
1123

1124
    Ok(result_grid)
844✔
1125
}
846✔
1126

1127
/// 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.
1128
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
847✔
1129
    dataset: &GdalDataset,
847✔
1130
    dataset_params: &GdalDatasetParameters,
847✔
1131
    tile_info: TileInformation,
847✔
1132
    tile_time: TimeInterval,
847✔
1133
    cache_hint: CacheHint,
847✔
1134
) -> Result<RasterTile2D<T>> {
847✔
1135
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel)?;
847✔
1136

1137
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
846✔
1138

1139
    let mut properties = RasterProperties::default();
844✔
1140

844✔
1141
    // always read the scale and offset values from the rasterband
844✔
1142
    properties_from_band(&mut properties, &rasterband);
844✔
1143

1144
    // read the properties from the dataset and rasterband metadata
1145
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
844✔
1146
        properties_from_gdal_metadata(&mut properties, dataset, properties_mapping);
4✔
1147
        properties_from_gdal_metadata(&mut properties, &rasterband, properties_mapping);
4✔
1148
    }
840✔
1149

1150
    // TODO: add cache_hint
1151
    Ok(RasterTile2D::new_with_tile_info_and_properties(
844✔
1152
        tile_time,
844✔
1153
        tile_info,
844✔
1154
        0,
844✔
1155
        result_grid,
844✔
1156
        properties,
844✔
1157
        cache_hint,
844✔
1158
    ))
844✔
1159
}
847✔
1160

1161
fn create_no_data_tile<T: Pixel>(
81✔
1162
    tile_info: TileInformation,
81✔
1163
    tile_time: TimeInterval,
81✔
1164
    cache_hint: CacheHint,
81✔
1165
) -> RasterTile2D<T> {
81✔
1166
    // TODO: add cache_hint
81✔
1167
    RasterTile2D::new_with_tile_info_and_properties(
81✔
1168
        tile_time,
81✔
1169
        tile_info,
81✔
1170
        0,
81✔
1171
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
81✔
1172
        RasterProperties::default(),
81✔
1173
        cache_hint,
81✔
1174
    )
81✔
1175
}
81✔
1176

UNCOV
1177
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
×
1178
pub struct GdalMetadataMapping {
1179
    pub source_key: RasterPropertiesKey,
1180
    pub target_key: RasterPropertiesKey,
1181
    pub target_type: RasterPropertiesEntryType,
1182
}
1183

1184
impl GdalMetadataMapping {
1185
    pub fn identity(
×
1186
        key: RasterPropertiesKey,
×
1187
        target_type: RasterPropertiesEntryType,
×
1188
    ) -> GdalMetadataMapping {
×
1189
        GdalMetadataMapping {
×
1190
            source_key: key.clone(),
×
1191
            target_key: key,
×
1192
            target_type,
×
1193
        }
×
1194
    }
×
1195
}
1196

1197
fn properties_from_gdal_metadata<'a, I, M>(
8✔
1198
    properties: &mut RasterProperties,
8✔
1199
    gdal_dataset: &M,
8✔
1200
    properties_mapping: I,
8✔
1201
) where
8✔
1202
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1203
    M: GdalMetadata,
8✔
1204
{
8✔
1205
    let mapping_iter = properties_mapping.into_iter();
8✔
1206

1207
    for m in mapping_iter {
24✔
1208
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1209
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1210
        } else {
1211
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1212
        };
1213

1214
        if let Some(d) = data {
16✔
1215
            let entry = match m.target_type {
8✔
1216
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1217
                    |_| RasterPropertiesEntry::String(d),
×
1218
                    RasterPropertiesEntry::Number,
×
1219
                ),
×
1220
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1221
            };
1222

1223
            debug!(
8✔
1224
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1225
                &m.source_key, &m.target_key, &entry
×
1226
            );
1227

1228
            properties.insert_property(m.target_key.clone(), entry);
8✔
1229
        }
8✔
1230
    }
1231
}
8✔
1232

1233
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
844✔
1234
    if let Some(scale) = gdal_dataset.scale() {
844✔
1235
        properties.set_scale(scale);
1✔
1236
    };
843✔
1237
    if let Some(offset) = gdal_dataset.offset() {
844✔
1238
        properties.set_offset(offset);
1✔
1239
    };
843✔
1240

1241
    // ignore if there is no description
1242
    if let Ok(description) = gdal_dataset.description() {
844✔
1243
        properties.set_description(description);
844✔
1244
    }
844✔
1245
}
844✔
1246

1247
#[cfg(test)]
1248
mod tests {
1249
    use super::*;
1250
    use crate::engine::{MockExecutionContext, MockQueryContext};
1251
    use crate::test_data;
1252
    use crate::util::gdal::add_ndvi_dataset;
1253
    use crate::util::Result;
1254
    use geoengine_datatypes::hashmap;
1255
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1256
    use geoengine_datatypes::raster::{
1257
        EmptyGrid2D, GridBounds, GridIdx2D, TilesEqualIgnoringCacheHint,
1258
    };
1259
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1260
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1261
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1262
    use httptest::matchers::request;
1263
    use httptest::{responders, Expectation, Server};
1264

1265
    async fn query_gdal_source(
5✔
1266
        exe_ctx: &MockExecutionContext,
5✔
1267
        query_ctx: &MockQueryContext,
5✔
1268
        name: NamedData,
5✔
1269
        output_shape: GridShape2D,
5✔
1270
        output_bounds: SpatialPartition2D,
5✔
1271
        time_interval: TimeInterval,
5✔
1272
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1273
        let op = GdalSource {
5✔
1274
            params: GdalSourceParameters { data: name.clone() },
5✔
1275
        }
5✔
1276
        .boxed();
5✔
1277

5✔
1278
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1279
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1280
        let spatial_resolution =
5✔
1281
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1282

1283
        let o = op
5✔
1284
            .initialize(WorkflowOperatorPath::initialize_root(), exe_ctx)
5✔
1285
            .await
5✔
1286
            .unwrap();
5✔
1287

5✔
1288
        o.query_processor()
5✔
1289
            .unwrap()
5✔
1290
            .get_u8()
5✔
1291
            .unwrap()
5✔
1292
            .raster_query(
5✔
1293
                RasterQueryRectangle {
5✔
1294
                    spatial_bounds: output_bounds,
5✔
1295
                    time_interval,
5✔
1296
                    spatial_resolution,
5✔
1297
                    attributes: BandSelection::first(),
5✔
1298
                },
5✔
1299
                query_ctx,
5✔
1300
            )
5✔
1301
            .await
5✔
1302
            .unwrap()
5✔
1303
            .collect()
5✔
1304
            .await
5✔
1305
    }
5✔
1306

1307
    fn load_ndvi_jan_2014(
3✔
1308
        output_shape: GridShape2D,
3✔
1309
        output_bounds: SpatialPartition2D,
3✔
1310
    ) -> Result<RasterTile2D<u8>> {
3✔
1311
        GdalRasterLoader::load_tile_data::<u8>(
3✔
1312
            &GdalDatasetParameters {
3✔
1313
                file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
3✔
1314
                rasterband_channel: 1,
3✔
1315
                geo_transform: GdalDatasetGeoTransform {
3✔
1316
                    origin_coordinate: (-180., 90.).into(),
3✔
1317
                    x_pixel_size: 0.1,
3✔
1318
                    y_pixel_size: -0.1,
3✔
1319
                },
3✔
1320
                width: 3600,
3✔
1321
                height: 1800,
3✔
1322
                file_not_found_handling: FileNotFoundHandling::NoData,
3✔
1323
                no_data_value: Some(0.),
3✔
1324
                properties_mapping: Some(vec![
3✔
1325
                    GdalMetadataMapping {
3✔
1326
                        source_key: RasterPropertiesKey {
3✔
1327
                            domain: None,
3✔
1328
                            key: "AREA_OR_POINT".to_string(),
3✔
1329
                        },
3✔
1330
                        target_type: RasterPropertiesEntryType::String,
3✔
1331
                        target_key: RasterPropertiesKey {
3✔
1332
                            domain: None,
3✔
1333
                            key: "AREA_OR_POINT".to_string(),
3✔
1334
                        },
3✔
1335
                    },
3✔
1336
                    GdalMetadataMapping {
3✔
1337
                        source_key: RasterPropertiesKey {
3✔
1338
                            domain: Some("IMAGE_STRUCTURE".to_string()),
3✔
1339
                            key: "COMPRESSION".to_string(),
3✔
1340
                        },
3✔
1341
                        target_type: RasterPropertiesEntryType::String,
3✔
1342
                        target_key: RasterPropertiesKey {
3✔
1343
                            domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
3✔
1344
                            key: "COMPRESSION".to_string(),
3✔
1345
                        },
3✔
1346
                    },
3✔
1347
                ]),
3✔
1348
                gdal_open_options: None,
3✔
1349
                gdal_config_options: None,
3✔
1350
                allow_alphaband_as_mask: true,
3✔
1351
                retry: None,
3✔
1352
            },
3✔
1353
            TileInformation::with_partition_and_shape(output_bounds, output_shape),
3✔
1354
            TimeInterval::default(),
3✔
1355
            CacheHint::default(),
3✔
1356
        )
3✔
1357
    }
3✔
1358

1359
    #[test]
1360
    fn tiling_strategy_origin() {
1✔
1361
        let tile_size_in_pixels = [600, 600];
1✔
1362
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1363
        let dataset_x_pixel_size = 0.1;
1✔
1364
        let dataset_y_pixel_size = -0.1;
1✔
1365
        let dataset_geo_transform = GeoTransform::new(
1✔
1366
            dataset_upper_right_coord,
1✔
1367
            dataset_x_pixel_size,
1✔
1368
            dataset_y_pixel_size,
1✔
1369
        );
1✔
1370

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

1✔
1373
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1374
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1375
            geo_transform: dataset_geo_transform,
1✔
1376
        };
1✔
1377

1✔
1378
        assert_eq!(
1✔
1379
            origin_split_tileing_strategy
1✔
1380
                .geo_transform
1✔
1381
                .upper_left_pixel_idx(&partition),
1✔
1382
            [0, 0].into()
1✔
1383
        );
1✔
1384
        assert_eq!(
1✔
1385
            origin_split_tileing_strategy
1✔
1386
                .geo_transform
1✔
1387
                .lower_right_pixel_idx(&partition),
1✔
1388
            [1800 - 1, 3600 - 1].into()
1✔
1389
        );
1✔
1390

1391
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1392
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1393
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1394
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1395
    }
1✔
1396

1397
    #[test]
1398
    fn tiling_strategy_zero() {
1✔
1399
        let tile_size_in_pixels = [600, 600];
1✔
1400
        let dataset_x_pixel_size = 0.1;
1✔
1401
        let dataset_y_pixel_size = -0.1;
1✔
1402
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1403
            0.0,
1✔
1404
            dataset_x_pixel_size,
1✔
1405
            0.0,
1✔
1406
            dataset_y_pixel_size,
1✔
1407
        );
1✔
1408

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

1✔
1411
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1412
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1413
            geo_transform: central_geo_transform,
1✔
1414
        };
1✔
1415

1✔
1416
        assert_eq!(
1✔
1417
            origin_split_tileing_strategy
1✔
1418
                .geo_transform
1✔
1419
                .upper_left_pixel_idx(&partition),
1✔
1420
            [-900, -1800].into()
1✔
1421
        );
1✔
1422
        assert_eq!(
1✔
1423
            origin_split_tileing_strategy
1✔
1424
                .geo_transform
1✔
1425
                .lower_right_pixel_idx(&partition),
1✔
1426
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1427
        );
1✔
1428

1429
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1430
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1431
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1432
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1433
    }
1✔
1434

1435
    #[test]
1436
    fn tile_idx_iterator() {
1✔
1437
        let tile_size_in_pixels = [600, 600];
1✔
1438
        let dataset_x_pixel_size = 0.1;
1✔
1439
        let dataset_y_pixel_size = -0.1;
1✔
1440
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1441
            0.0,
1✔
1442
            dataset_x_pixel_size,
1✔
1443
            0.0,
1✔
1444
            dataset_y_pixel_size,
1✔
1445
        );
1✔
1446

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

1✔
1449
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1450
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1451
            geo_transform: central_geo_transform,
1✔
1452
        };
1✔
1453

1✔
1454
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1455
            .tile_idx_iterator(partition)
1✔
1456
            .collect();
1✔
1457
        assert_eq!(vres.len(), 4 * 6);
1✔
1458
        assert_eq!(vres[0], [-2, -3].into());
1✔
1459
        assert_eq!(vres[1], [-2, -2].into());
1✔
1460
        assert_eq!(vres[2], [-2, -1].into());
1✔
1461
        assert_eq!(vres[23], [1, 2].into());
1✔
1462
    }
1✔
1463

1464
    #[test]
1465
    fn tile_information_iterator() {
1✔
1466
        let tile_size_in_pixels = [600, 600];
1✔
1467
        let dataset_x_pixel_size = 0.1;
1✔
1468
        let dataset_y_pixel_size = -0.1;
1✔
1469

1✔
1470
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1471
            0.0,
1✔
1472
            dataset_x_pixel_size,
1✔
1473
            0.0,
1✔
1474
            dataset_y_pixel_size,
1✔
1475
        );
1✔
1476

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

1✔
1479
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1480
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1481
            geo_transform: central_geo_transform,
1✔
1482
        };
1✔
1483

1✔
1484
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1485
            .tile_information_iterator(partition)
1✔
1486
            .collect();
1✔
1487
        assert_eq!(vres.len(), 4 * 6);
1✔
1488
        assert_eq!(
1✔
1489
            vres[0],
1✔
1490
            TileInformation::new(
1✔
1491
                [-2, -3].into(),
1✔
1492
                tile_size_in_pixels.into(),
1✔
1493
                central_geo_transform,
1✔
1494
            )
1✔
1495
        );
1✔
1496
        assert_eq!(
1✔
1497
            vres[1],
1✔
1498
            TileInformation::new(
1✔
1499
                [-2, -2].into(),
1✔
1500
                tile_size_in_pixels.into(),
1✔
1501
                central_geo_transform,
1✔
1502
            )
1✔
1503
        );
1✔
1504
        assert_eq!(
1✔
1505
            vres[12],
1✔
1506
            TileInformation::new(
1✔
1507
                [0, -3].into(),
1✔
1508
                tile_size_in_pixels.into(),
1✔
1509
                central_geo_transform,
1✔
1510
            )
1✔
1511
        );
1✔
1512
        assert_eq!(
1✔
1513
            vres[23],
1✔
1514
            TileInformation::new(
1✔
1515
                [1, 2].into(),
1✔
1516
                tile_size_in_pixels.into(),
1✔
1517
                central_geo_transform,
1✔
1518
            )
1✔
1519
        );
1✔
1520
    }
1✔
1521

1522
    #[test]
1523
    fn replace_time_placeholder() {
1✔
1524
        let params = GdalDatasetParameters {
1✔
1525
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1526
            rasterband_channel: 0,
1✔
1527
            geo_transform: TestDefault::test_default(),
1✔
1528
            width: 360,
1✔
1529
            height: 180,
1✔
1530
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1531
            no_data_value: Some(0.),
1✔
1532
            properties_mapping: None,
1✔
1533
            gdal_open_options: None,
1✔
1534
            gdal_config_options: None,
1✔
1535
            allow_alphaband_as_mask: true,
1✔
1536
            retry: None,
1✔
1537
        };
1✔
1538
        let replaced = params
1✔
1539
            .replace_time_placeholders(
1✔
1540
                &hashmap! {
1✔
1541
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1542
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1543
                        reference: TimeReference::Start,
1✔
1544
                    },
1✔
1545
                },
1✔
1546
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1547
            )
1✔
1548
            .unwrap();
1✔
1549
        assert_eq!(
1✔
1550
            replaced.file_path.to_string_lossy(),
1✔
1551
            "/foo/bar_022000000.tiff".to_string()
1✔
1552
        );
1✔
1553
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1554
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1555
        assert_eq!(params.width, replaced.width);
1✔
1556
        assert_eq!(params.height, replaced.height);
1✔
1557
        assert_eq!(
1✔
1558
            params.file_not_found_handling,
1✔
1559
            replaced.file_not_found_handling
1✔
1560
        );
1✔
1561
    }
1✔
1562

1563
    #[test]
1564
    fn test_load_tile_data() {
1✔
1565
        let output_shape: GridShape2D = [8, 8].into();
1✔
1566
        let output_bounds =
1✔
1567
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1568

1✔
1569
        let RasterTile2D {
1✔
1570
            global_geo_transform: _,
1✔
1571
            grid_array: grid,
1✔
1572
            tile_position: _,
1✔
1573
            band: _,
1✔
1574
            time: _,
1✔
1575
            properties,
1✔
1576
            cache_hint: _,
1✔
1577
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1578

1✔
1579
        assert!(!grid.is_empty());
1✔
1580

1581
        let grid = grid.into_materialized_masked_grid();
1✔
1582

1✔
1583
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1584
        assert_eq!(
1✔
1585
            grid.inner_grid.data,
1✔
1586
            &[
1✔
1587
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1588
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1589
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1590
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1591
            ]
1✔
1592
        );
1✔
1593

1594
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1595
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1596

1597
        assert!((properties.scale_option()).is_none());
1✔
1598
        assert!(properties.offset_option().is_none());
1✔
1599
        assert_eq!(
1✔
1600
            properties.get_property(&RasterPropertiesKey {
1✔
1601
                key: "AREA_OR_POINT".to_string(),
1✔
1602
                domain: None,
1✔
1603
            }),
1✔
1604
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1605
        );
1✔
1606
        assert_eq!(
1✔
1607
            properties.get_property(&RasterPropertiesKey {
1✔
1608
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1609
                key: "COMPRESSION".to_string(),
1✔
1610
            }),
1✔
1611
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1612
        );
1✔
1613
    }
1✔
1614

1615
    #[test]
1616
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1617
        let output_shape: GridShape2D = [8, 8].into();
1✔
1618
        // shift world bbox one pixel up and to the left
1✔
1619
        let (x_size, y_size) = (45., 22.5);
1✔
1620
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1621
            (-180. - x_size, 90. + y_size).into(),
1✔
1622
            (180. - x_size, -90. + y_size).into(),
1✔
1623
        );
1✔
1624

1✔
1625
        let RasterTile2D {
1✔
1626
            global_geo_transform: _,
1✔
1627
            grid_array: grid,
1✔
1628
            tile_position: _,
1✔
1629
            band: _,
1✔
1630
            time: _,
1✔
1631
            properties: _,
1✔
1632
            cache_hint: _,
1✔
1633
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1634

1✔
1635
        assert!(!grid.is_empty());
1✔
1636

1637
        let x = grid.into_materialized_masked_grid();
1✔
1638

1✔
1639
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1640
        assert_eq!(
1✔
1641
            x.inner_grid.data,
1✔
1642
            &[
1✔
1643
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1644
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1645
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1646
                255, 255, 255, 255, 255, 255
1✔
1647
            ]
1✔
1648
        );
1✔
1649
    }
1✔
1650

1651
    #[test]
1652
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1653
        let output_shape: GridShape2D = [8, 8].into();
1✔
1654
        // shift world bbox one pixel up and to the left
1✔
1655
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1656
        let output_bounds = SpatialPartition2D::new(
1✔
1657
            (-116.22222, 66.66666).into(),
1✔
1658
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1659
        )
1✔
1660
        .unwrap();
1✔
1661

1✔
1662
        let RasterTile2D {
1✔
1663
            global_geo_transform: _,
1✔
1664
            grid_array: grid,
1✔
1665
            tile_position: _,
1✔
1666
            band: _,
1✔
1667
            time: _,
1✔
1668
            properties: _,
1✔
1669
            cache_hint: _,
1✔
1670
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1671

1✔
1672
        assert!(!grid.is_empty());
1✔
1673

1674
        let x = grid.into_materialized_masked_grid();
1✔
1675

1✔
1676
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1677
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1678
    }
1✔
1679

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

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

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

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

1✔
1704
        assert_eq!(
1✔
1705
            c[0].time,
1✔
1706
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1707
        );
1✔
1708

1✔
1709
        assert_eq!(
1✔
1710
            c[0].tile_information().global_tile_position(),
1✔
1711
            [-1, -1].into()
1✔
1712
        );
1✔
1713

1✔
1714
        assert_eq!(
1✔
1715
            c[1].tile_information().global_tile_position(),
1✔
1716
            [-1, 0].into()
1✔
1717
        );
1✔
1718

1✔
1719
        assert_eq!(
1✔
1720
            c[2].tile_information().global_tile_position(),
1✔
1721
            [0, -1].into()
1✔
1722
        );
1✔
1723

1✔
1724
        assert_eq!(
1✔
1725
            c[3].tile_information().global_tile_position(),
1✔
1726
            [0, 0].into()
1✔
1727
        );
1✔
1728
    }
1✔
1729

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

1✔
1736
        let output_shape: GridShape2D = [256, 256].into();
1✔
1737
        let output_bounds =
1✔
1738
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1739
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-01
1✔
1740

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

1✔
1752
        assert_eq!(c.len(), 8);
1✔
1753

1✔
1754
        assert_eq!(
1✔
1755
            c[0].time,
1✔
1756
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1757
        );
1✔
1758

1✔
1759
        assert_eq!(
1✔
1760
            c[5].time,
1✔
1761
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1762
        );
1✔
1763
    }
1✔
1764

1765
    #[tokio::test]
1766
    async fn test_query_before_data() {
1✔
1767
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1768
        let query_ctx = MockQueryContext::test_default();
1✔
1769
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1770

1✔
1771
        let output_shape: GridShape2D = [256, 256].into();
1✔
1772
        let output_bounds =
1✔
1773
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1774
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1775

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

1✔
1787
        assert_eq!(c.len(), 4);
1✔
1788

1✔
1789
        assert_eq!(
1✔
1790
            c[0].time,
1✔
1791
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1792
        );
1✔
1793
    }
1✔
1794

1795
    #[tokio::test]
1796
    async fn test_query_after_data() {
1✔
1797
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1798
        let query_ctx = MockQueryContext::test_default();
1✔
1799
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1800

1✔
1801
        let output_shape: GridShape2D = [256, 256].into();
1✔
1802
        let output_bounds =
1✔
1803
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1804
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1805

1✔
1806
        let c = query_gdal_source(
1✔
1807
            &exe_ctx,
1✔
1808
            &query_ctx,
1✔
1809
            id,
1✔
1810
            output_shape,
1✔
1811
            output_bounds,
1✔
1812
            time_interval,
1✔
1813
        )
1✔
1814
        .await;
1✔
1815
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1816

1✔
1817
        assert_eq!(c.len(), 4);
1✔
1818

1✔
1819
        assert_eq!(
1✔
1820
            c[0].time,
1✔
1821
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1822
        );
1✔
1823
    }
1✔
1824

1825
    #[tokio::test]
1826
    async fn test_nodata() {
1✔
1827
        hide_gdal_errors();
1✔
1828

1✔
1829
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1830
        let query_ctx = MockQueryContext::test_default();
1✔
1831
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1832

1✔
1833
        let output_shape: GridShape2D = [256, 256].into();
1✔
1834
        let output_bounds =
1✔
1835
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1836
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1837

1✔
1838
        let c = query_gdal_source(
1✔
1839
            &exe_ctx,
1✔
1840
            &query_ctx,
1✔
1841
            id,
1✔
1842
            output_shape,
1✔
1843
            output_bounds,
1✔
1844
            time_interval,
1✔
1845
        )
1✔
1846
        .await;
1✔
1847
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1848

1✔
1849
        assert_eq!(c.len(), 4);
1✔
1850

1✔
1851
        let tile_1 = &c[0];
1✔
1852

1✔
1853
        assert_eq!(
1✔
1854
            tile_1.time,
1✔
1855
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1856
        );
1✔
1857

1✔
1858
        assert!(tile_1.is_empty());
1✔
1859
    }
1✔
1860

1861
    #[tokio::test]
1862
    async fn timestep_without_params() {
1✔
1863
        let output_bounds =
1✔
1864
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1865
        let output_shape: GridShape2D = [256, 256].into();
1✔
1866

1✔
1867
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1868
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1869
        let params = None;
1✔
1870

1✔
1871
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
1872
            params,
1✔
1873
            tile_info,
1✔
1874
            time_interval,
1✔
1875
            CacheHint::default(),
1✔
1876
        )
1✔
1877
        .await;
1✔
1878

1✔
1879
        assert!(tile.is_ok());
1✔
1880

1✔
1881
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1882
            time_interval,
1✔
1883
            tile_info,
1✔
1884
            0,
1✔
1885
            EmptyGrid2D::new(output_shape).into(),
1✔
1886
            CacheHint::default(),
1✔
1887
        );
1✔
1888

1✔
1889
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
1890
    }
1✔
1891

1892
    #[test]
1893
    fn it_reverts_config_options() {
1✔
1894
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1895

1✔
1896
        {
1✔
1897
            let _config =
1✔
1898
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1899

1✔
1900
            assert_eq!(
1✔
1901
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1902
                "bar".to_owned()
1✔
1903
            );
1✔
1904
        }
1905

1906
        assert_eq!(
1✔
1907
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1908
            String::new()
1✔
1909
        );
1✔
1910
    }
1✔
1911

1912
    #[test]
1913
    #[allow(clippy::too_many_lines)]
1914
    fn deserialize_dataset_parameters() {
1✔
1915
        let dataset_parameters = GdalDatasetParameters {
1✔
1916
            file_path: "path-to-data.tiff".into(),
1✔
1917
            rasterband_channel: 1,
1✔
1918
            geo_transform: GdalDatasetGeoTransform {
1✔
1919
                origin_coordinate: (-180., 90.).into(),
1✔
1920
                x_pixel_size: 0.1,
1✔
1921
                y_pixel_size: -0.1,
1✔
1922
            },
1✔
1923
            width: 3600,
1✔
1924
            height: 1800,
1✔
1925
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1926
            no_data_value: Some(f64::NAN),
1✔
1927
            properties_mapping: Some(vec![
1✔
1928
                GdalMetadataMapping {
1✔
1929
                    source_key: RasterPropertiesKey {
1✔
1930
                        domain: None,
1✔
1931
                        key: "AREA_OR_POINT".to_string(),
1✔
1932
                    },
1✔
1933
                    target_type: RasterPropertiesEntryType::String,
1✔
1934
                    target_key: RasterPropertiesKey {
1✔
1935
                        domain: None,
1✔
1936
                        key: "AREA_OR_POINT".to_string(),
1✔
1937
                    },
1✔
1938
                },
1✔
1939
                GdalMetadataMapping {
1✔
1940
                    source_key: RasterPropertiesKey {
1✔
1941
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
1942
                        key: "COMPRESSION".to_string(),
1✔
1943
                    },
1✔
1944
                    target_type: RasterPropertiesEntryType::String,
1✔
1945
                    target_key: RasterPropertiesKey {
1✔
1946
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1947
                        key: "COMPRESSION".to_string(),
1✔
1948
                    },
1✔
1949
                },
1✔
1950
            ]),
1✔
1951
            gdal_open_options: None,
1✔
1952
            gdal_config_options: None,
1✔
1953
            allow_alphaband_as_mask: true,
1✔
1954
            retry: None,
1✔
1955
        };
1✔
1956

1✔
1957
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1958

1✔
1959
        assert_eq!(
1✔
1960
            dataset_parameters_json,
1✔
1961
            serde_json::json!({
1✔
1962
                "filePath": "path-to-data.tiff",
1✔
1963
                "rasterbandChannel": 1,
1✔
1964
                "geoTransform": {
1✔
1965
                    "originCoordinate": {
1✔
1966
                        "x": -180.,
1✔
1967
                        "y": 90.
1✔
1968
                    },
1✔
1969
                    "xPixelSize": 0.1,
1✔
1970
                    "yPixelSize": -0.1
1✔
1971
                },
1✔
1972
                "width": 3600,
1✔
1973
                "height": 1800,
1✔
1974
                "fileNotFoundHandling": "NoData",
1✔
1975
                "noDataValue": "nan",
1✔
1976
                "propertiesMapping": [{
1✔
1977
                        "source_key": {
1✔
1978
                            "domain": null,
1✔
1979
                            "key": "AREA_OR_POINT"
1✔
1980
                        },
1✔
1981
                        "target_key": {
1✔
1982
                            "domain": null,
1✔
1983
                            "key": "AREA_OR_POINT"
1✔
1984
                        },
1✔
1985
                        "target_type": "String"
1✔
1986
                    },
1✔
1987
                    {
1✔
1988
                        "source_key": {
1✔
1989
                            "domain": "IMAGE_STRUCTURE",
1✔
1990
                            "key": "COMPRESSION"
1✔
1991
                        },
1✔
1992
                        "target_key": {
1✔
1993
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1994
                            "key": "COMPRESSION"
1✔
1995
                        },
1✔
1996
                        "target_type": "String"
1✔
1997
                    }
1✔
1998
                ],
1✔
1999
                "gdalOpenOptions": null,
1✔
2000
                "gdalConfigOptions": null,
1✔
2001
                "allowAlphabandAsMask": true,
1✔
2002
                "retry": null,
1✔
2003
            })
1✔
2004
        );
1✔
2005

2006
        let deserialized_parameters =
1✔
2007
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
2008

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

1✔
2011
        assert_eq!(
1✔
2012
            deserialized_parameters.file_path,
1✔
2013
            dataset_parameters.file_path,
1✔
2014
        );
1✔
2015
        assert_eq!(
1✔
2016
            deserialized_parameters.rasterband_channel,
1✔
2017
            dataset_parameters.rasterband_channel,
1✔
2018
        );
1✔
2019
        assert_eq!(
1✔
2020
            deserialized_parameters.geo_transform,
1✔
2021
            dataset_parameters.geo_transform,
1✔
2022
        );
1✔
2023
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
2024
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
2025
        assert_eq!(
1✔
2026
            deserialized_parameters.file_not_found_handling,
1✔
2027
            dataset_parameters.file_not_found_handling,
1✔
2028
        );
1✔
2029
        assert!(
1✔
2030
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
2031
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
2032
        );
2033
        assert_eq!(
1✔
2034
            deserialized_parameters.properties_mapping,
1✔
2035
            dataset_parameters.properties_mapping,
1✔
2036
        );
1✔
2037
        assert_eq!(
1✔
2038
            deserialized_parameters.gdal_open_options,
1✔
2039
            dataset_parameters.gdal_open_options,
1✔
2040
        );
1✔
2041
        assert_eq!(
1✔
2042
            deserialized_parameters.gdal_config_options,
1✔
2043
            dataset_parameters.gdal_config_options,
1✔
2044
        );
1✔
2045
    }
1✔
2046

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

1✔
2055
        let sb = gt.spatial_partition(10, 10);
1✔
2056

1✔
2057
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
2058
            .unwrap();
1✔
2059

1✔
2060
        assert_eq!(sb, exp);
1✔
2061
    }
1✔
2062

2063
    #[test]
2064
    fn gdal_geotransform_to_bounds_neg_y_5() {
1✔
2065
        let gt = GdalDatasetGeoTransform {
1✔
2066
            origin_coordinate: Coordinate2D::new(5., 5.),
1✔
2067
            x_pixel_size: 0.5,
1✔
2068
            y_pixel_size: -0.5,
1✔
2069
        };
1✔
2070

1✔
2071
        let sb = gt.spatial_partition(10, 10);
1✔
2072

1✔
2073
        let exp =
1✔
2074
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1✔
2075

1✔
2076
        assert_eq!(sb, exp);
1✔
2077
    }
1✔
2078

2079
    #[test]
2080
    fn gdal_geotransform_to_bounds_pos_y_0() {
1✔
2081
        let gt = GdalDatasetGeoTransform {
1✔
2082
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2083
            x_pixel_size: 1.,
1✔
2084
            y_pixel_size: 1.,
1✔
2085
        };
1✔
2086

1✔
2087
        let sb = gt.spatial_partition(10, 10);
1✔
2088

1✔
2089
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2090
            .unwrap();
1✔
2091

1✔
2092
        assert_eq!(sb, exp);
1✔
2093
    }
1✔
2094

2095
    #[test]
2096
    fn gdal_geotransform_to_bounds_pos_y_5() {
1✔
2097
        let gt = GdalDatasetGeoTransform {
1✔
2098
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
2099
            x_pixel_size: 0.5,
1✔
2100
            y_pixel_size: 0.5,
1✔
2101
        };
1✔
2102

1✔
2103
        let sb = gt.spatial_partition(10, 10);
1✔
2104

1✔
2105
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
2106
            .unwrap();
1✔
2107

1✔
2108
        assert_eq!(sb, exp);
1✔
2109
    }
1✔
2110

2111
    #[test]
2112
    fn gdal_read_window_data_origin_upper_left() {
1✔
2113
        let gt = GdalDatasetGeoTransform {
1✔
2114
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
2115
            x_pixel_size: 0.5,
1✔
2116
            y_pixel_size: -0.5,
1✔
2117
        };
1✔
2118

1✔
2119
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
2120
            .unwrap();
1✔
2121

1✔
2122
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2123

1✔
2124
        let exp = GdalReadWindow {
1✔
2125
            size_x: 4,
1✔
2126
            size_y: 6,
1✔
2127
            start_x: 6,
1✔
2128
            start_y: 4,
1✔
2129
        };
1✔
2130

1✔
2131
        assert_eq!(rw, exp);
1✔
2132
    }
1✔
2133

2134
    #[test]
2135
    fn gdal_read_window_data_origin_lower_left() {
1✔
2136
        let gt = GdalDatasetGeoTransform {
1✔
2137
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2138
            x_pixel_size: 1.,
1✔
2139
            y_pixel_size: 1.,
1✔
2140
        };
1✔
2141

1✔
2142
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2143
            .unwrap();
1✔
2144

1✔
2145
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2146

1✔
2147
        let exp = GdalReadWindow {
1✔
2148
            size_x: 10,
1✔
2149
            size_y: 10,
1✔
2150
            start_x: 0,
1✔
2151
            start_y: 0,
1✔
2152
        };
1✔
2153

1✔
2154
        assert_eq!(rw, exp);
1✔
2155
    }
1✔
2156

2157
    #[test]
2158
    fn read_up_side_down_raster() {
1✔
2159
        let output_shape: GridShape2D = [8, 8].into();
1✔
2160
        let output_bounds =
1✔
2161
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2162

1✔
2163
        let up_side_down_params = GdalDatasetParameters {
1✔
2164
            file_path: test_data!(
1✔
2165
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2166
            )
1✔
2167
            .into(),
1✔
2168
            rasterband_channel: 1,
1✔
2169
            geo_transform: GdalDatasetGeoTransform {
1✔
2170
                origin_coordinate: (-180., -90.).into(),
1✔
2171
                x_pixel_size: 0.1,
1✔
2172
                y_pixel_size: 0.1,
1✔
2173
            },
1✔
2174
            width: 3600,
1✔
2175
            height: 1800,
1✔
2176
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2177
            no_data_value: Some(0.),
1✔
2178
            properties_mapping: Some(vec![
1✔
2179
                GdalMetadataMapping {
1✔
2180
                    source_key: RasterPropertiesKey {
1✔
2181
                        domain: None,
1✔
2182
                        key: "AREA_OR_POINT".to_string(),
1✔
2183
                    },
1✔
2184
                    target_type: RasterPropertiesEntryType::String,
1✔
2185
                    target_key: RasterPropertiesKey {
1✔
2186
                        domain: None,
1✔
2187
                        key: "AREA_OR_POINT".to_string(),
1✔
2188
                    },
1✔
2189
                },
1✔
2190
                GdalMetadataMapping {
1✔
2191
                    source_key: RasterPropertiesKey {
1✔
2192
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2193
                        key: "COMPRESSION".to_string(),
1✔
2194
                    },
1✔
2195
                    target_type: RasterPropertiesEntryType::String,
1✔
2196
                    target_key: RasterPropertiesKey {
1✔
2197
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2198
                        key: "COMPRESSION".to_string(),
1✔
2199
                    },
1✔
2200
                },
1✔
2201
            ]),
1✔
2202
            gdal_open_options: None,
1✔
2203
            gdal_config_options: None,
1✔
2204
            allow_alphaband_as_mask: true,
1✔
2205
            retry: None,
1✔
2206
        };
1✔
2207

1✔
2208
        let tile_information =
1✔
2209
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2210

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

1✔
2227
        assert!(!grid.is_empty());
1✔
2228

2229
        let grid = grid.into_materialized_masked_grid();
1✔
2230

1✔
2231
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2232
        assert_eq!(
1✔
2233
            grid.inner_grid.data,
1✔
2234
            &[
1✔
2235
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2236
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2237
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2238
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2239
            ]
1✔
2240
        );
1✔
2241

2242
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2243
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2244

2245
        assert!(properties.offset_option().is_none());
1✔
2246
        assert!(properties.scale_option().is_none());
1✔
2247
    }
1✔
2248

2249
    #[test]
2250
    fn read_raster_and_offset_scale() {
1✔
2251
        let output_shape: GridShape2D = [8, 8].into();
1✔
2252
        let output_bounds =
1✔
2253
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2254

1✔
2255
        let up_side_down_params = GdalDatasetParameters {
1✔
2256
            file_path: test_data!(
1✔
2257
                "raster/modis_ndvi/with_offset_scale/MOD13A2_M_NDVI_2014-01-01.TIFF"
1✔
2258
            )
1✔
2259
            .into(),
1✔
2260
            rasterband_channel: 1,
1✔
2261
            geo_transform: GdalDatasetGeoTransform {
1✔
2262
                origin_coordinate: (-180., -90.).into(),
1✔
2263
                x_pixel_size: 0.1,
1✔
2264
                y_pixel_size: 0.1,
1✔
2265
            },
1✔
2266
            width: 3600,
1✔
2267
            height: 1800,
1✔
2268
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2269
            no_data_value: Some(0.),
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_information =
1✔
2278
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2279

1✔
2280
        let RasterTile2D {
1✔
2281
            global_geo_transform: _,
1✔
2282
            grid_array: grid,
1✔
2283
            tile_position: _,
1✔
2284
            band: _,
1✔
2285
            time: _,
1✔
2286
            properties,
1✔
2287
            cache_hint: _,
1✔
2288
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2289
            &up_side_down_params,
1✔
2290
            tile_information,
1✔
2291
            TimeInterval::default(),
1✔
2292
            CacheHint::default(),
1✔
2293
        )
1✔
2294
        .unwrap();
1✔
2295

1✔
2296
        assert!(!grid.is_empty());
1✔
2297

2298
        let grid = grid.into_materialized_masked_grid();
1✔
2299

1✔
2300
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2301
        assert_eq!(
1✔
2302
            grid.inner_grid.data,
1✔
2303
            &[
1✔
2304
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2305
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2306
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2307
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2308
            ]
1✔
2309
        );
1✔
2310

2311
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2312
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2313

2314
        assert_eq!(properties.offset_option(), Some(37.));
1✔
2315
        assert_eq!(properties.scale_option(), Some(3.7));
1✔
2316

2317
        assert!(approx_eq!(f64, properties.offset(), 37.));
1✔
2318
        assert!(approx_eq!(f64, properties.scale(), 3.7));
1✔
2319
    }
1✔
2320

2321
    #[test]
2322
    #[allow(clippy::too_many_lines)]
2323
    fn it_creates_no_data_only_for_missing_files() {
1✔
2324
        hide_gdal_errors();
1✔
2325

1✔
2326
        let ds = GdalDatasetParameters {
1✔
2327
            file_path: "nonexisting_file.tif".into(),
1✔
2328
            rasterband_channel: 1,
1✔
2329
            geo_transform: TestDefault::test_default(),
1✔
2330
            width: 100,
1✔
2331
            height: 100,
1✔
2332
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2333
            no_data_value: None,
1✔
2334
            properties_mapping: None,
1✔
2335
            gdal_open_options: None,
1✔
2336
            gdal_config_options: None,
1✔
2337
            allow_alphaband_as_mask: true,
1✔
2338
            retry: None,
1✔
2339
        };
1✔
2340

1✔
2341
        let tile_info = TileInformation {
1✔
2342
            tile_size_in_pixels: [100, 100].into(),
1✔
2343
            global_tile_position: [0, 0].into(),
1✔
2344
            global_geo_transform: TestDefault::test_default(),
1✔
2345
        };
1✔
2346

1✔
2347
        let tile_time = TimeInterval::default();
1✔
2348

1✔
2349
        // file doesn't exist => no data
1✔
2350
        let result =
1✔
2351
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2352
                .unwrap();
1✔
2353
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2354

2355
        let ds = GdalDatasetParameters {
1✔
2356
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2357
            rasterband_channel: 100, // invalid channel
1✔
2358
            geo_transform: TestDefault::test_default(),
1✔
2359
            width: 100,
1✔
2360
            height: 100,
1✔
2361
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2362
            no_data_value: None,
1✔
2363
            properties_mapping: None,
1✔
2364
            gdal_open_options: None,
1✔
2365
            gdal_config_options: None,
1✔
2366
            allow_alphaband_as_mask: true,
1✔
2367
            retry: None,
1✔
2368
        };
1✔
2369

1✔
2370
        // invalid channel => error
1✔
2371
        let result =
1✔
2372
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2373
        assert!(result.is_err());
1✔
2374

2375
        let server = Server::run();
1✔
2376

1✔
2377
        server.expect(
1✔
2378
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2379
                .times(1)
1✔
2380
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2381
        );
1✔
2382

1✔
2383
        server.expect(
1✔
2384
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2385
                .times(1)
1✔
2386
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2387
        );
1✔
2388

1✔
2389
        let ds = GdalDatasetParameters {
1✔
2390
            file_path: format!("/vsicurl/{}", server.url_str("/non_existing.tif")).into(),
1✔
2391
            rasterband_channel: 1,
1✔
2392
            geo_transform: TestDefault::test_default(),
1✔
2393
            width: 100,
1✔
2394
            height: 100,
1✔
2395
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2396
            no_data_value: None,
1✔
2397
            properties_mapping: None,
1✔
2398
            gdal_open_options: None,
1✔
2399
            gdal_config_options: Some(vec![
1✔
2400
                (
1✔
2401
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2402
                    ".tif".to_owned(),
1✔
2403
                ),
1✔
2404
                (
1✔
2405
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2406
                    "EMPTY_DIR".to_owned(),
1✔
2407
                ),
1✔
2408
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2409
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2410
            ]),
1✔
2411
            allow_alphaband_as_mask: true,
1✔
2412
            retry: None,
1✔
2413
        };
1✔
2414

1✔
2415
        // 404 => no data
1✔
2416
        let result =
1✔
2417
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2418
                .unwrap();
1✔
2419
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2420

2421
        let ds = GdalDatasetParameters {
1✔
2422
            file_path: format!("/vsicurl/{}", server.url_str("/internal_error.tif")).into(),
1✔
2423
            rasterband_channel: 1,
1✔
2424
            geo_transform: TestDefault::test_default(),
1✔
2425
            width: 100,
1✔
2426
            height: 100,
1✔
2427
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2428
            no_data_value: None,
1✔
2429
            properties_mapping: None,
1✔
2430
            gdal_open_options: None,
1✔
2431
            gdal_config_options: Some(vec![
1✔
2432
                (
1✔
2433
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2434
                    ".tif".to_owned(),
1✔
2435
                ),
1✔
2436
                (
1✔
2437
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2438
                    "EMPTY_DIR".to_owned(),
1✔
2439
                ),
1✔
2440
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2441
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2442
            ]),
1✔
2443
            allow_alphaband_as_mask: true,
1✔
2444
            retry: None,
1✔
2445
        };
1✔
2446

1✔
2447
        // 500 => error
1✔
2448
        let result =
1✔
2449
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2450
        assert!(result.is_err());
1✔
2451
    }
1✔
2452

2453
    #[test]
2454
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2455
        hide_gdal_errors();
1✔
2456

1✔
2457
        let server = Server::run();
1✔
2458

1✔
2459
        server.expect(
1✔
2460
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2461
                .times(2)
1✔
2462
                .respond_with(responders::cycle![
1✔
2463
                    // first generic error
1✔
2464
                    responders::status_code(500),
1✔
2465
                    // then 404 file not found
1✔
2466
                    responders::status_code(404)
1✔
2467
                ]),
1✔
2468
        );
1✔
2469

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

1✔
2472
        let options = Some(vec![
1✔
2473
            (
1✔
2474
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2475
                ".tif".to_owned(),
1✔
2476
            ),
1✔
2477
            (
1✔
2478
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2479
                "EMPTY_DIR".to_owned(),
1✔
2480
            ),
1✔
2481
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2482
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2483
        ]);
1✔
2484

1✔
2485
        let _thread_local_configs = options
1✔
2486
            .as_ref()
1✔
2487
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2488

1✔
2489
        // first fail
1✔
2490
        let result = gdal_open_dataset_ex(
1✔
2491
            file_path.as_path(),
1✔
2492
            DatasetOptions {
1✔
2493
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2494
                ..DatasetOptions::default()
1✔
2495
            },
1✔
2496
        );
1✔
2497

1✔
2498
        // it failed, but not with file not found
1✔
2499
        assert!(result.is_err());
1✔
2500
        if let Err(error) = result {
1✔
2501
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2502
        }
×
2503

2504
        // second fail doesn't even try, so still not "file not found", even though it should be now
2505
        let result = gdal_open_dataset_ex(
1✔
2506
            file_path.as_path(),
1✔
2507
            DatasetOptions {
1✔
2508
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2509
                ..DatasetOptions::default()
1✔
2510
            },
1✔
2511
        );
1✔
2512

1✔
2513
        assert!(result.is_err());
1✔
2514
        if let Err(error) = result {
1✔
2515
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2516
        }
×
2517

2518
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2519

1✔
2520
        // after clearing the cache, it tries again
1✔
2521
        let result = gdal_open_dataset_ex(
1✔
2522
            file_path.as_path(),
1✔
2523
            DatasetOptions {
1✔
2524
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2525
                ..DatasetOptions::default()
1✔
2526
            },
1✔
2527
        );
1✔
2528

1✔
2529
        // now we get the file not found error
1✔
2530
        assert!(result.is_err());
1✔
2531
        if let Err(error) = result {
1✔
2532
            assert!(error_is_gdal_file_not_found(&error));
1✔
2533
        }
×
2534
    }
1✔
2535

2536
    #[tokio::test]
2537
    async fn it_attaches_cache_hint() {
1✔
2538
        let output_bounds =
1✔
2539
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
2540
        let output_shape: GridShape2D = [256, 256].into();
1✔
2541

1✔
2542
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2543
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
2544
        let params = None;
1✔
2545

1✔
2546
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
2547
            params,
1✔
2548
            tile_info,
1✔
2549
            time_interval,
1✔
2550
            CacheHint::seconds(1234),
1✔
2551
        )
1✔
2552
        .await;
1✔
2553

1✔
2554
        assert!(tile.is_ok());
1✔
2555

1✔
2556
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
2557
            time_interval,
1✔
2558
            tile_info,
1✔
2559
            0,
1✔
2560
            EmptyGrid2D::new(output_shape).into(),
1✔
2561
            CacheHint::seconds(1234),
1✔
2562
        );
1✔
2563

1✔
2564
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
2565
    }
1✔
2566
}
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