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

geo-engine / geoengine / 11910714914

19 Nov 2024 10:06AM UTC coverage: 90.445% (-0.2%) from 90.687%
11910714914

push

github

web-flow
Merge pull request #994 from geo-engine/workspace-dependencies

use workspace dependencies, update toolchain, use global lock in expression

9 of 11 new or added lines in 6 files covered. (81.82%)

375 existing lines in 75 files now uncovered.

132867 of 146904 relevant lines covered (90.44%)

54798.11 hits per line

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

94.1
/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)]
76✔
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

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

121
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
541✔
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)]
65✔
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) {
852✔
169
        (self.start_x, self.start_y)
852✔
170
    }
852✔
171

172
    fn gdal_window_size(&self) -> (usize, usize) {
852✔
173
        (self.size_x, self.size_y)
852✔
174
    }
852✔
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.
182
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, FromSql, ToSql)]
3,531✔
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,717✔
193
        // the opposite y value (y value of the non origin edge)
1,717✔
194
        let opposite_coord_y = self.origin_coordinate.y + self.y_pixel_size * y_size as f64;
1,717✔
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,717✔
198
            (self.origin_coordinate.y, opposite_coord_y)
1,714✔
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,717✔
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,717✔
207
            (self.origin_coordinate.x, opposite_coord_x)
1,717✔
208
        } else {
209
            (opposite_coord_x, self.origin_coordinate.x)
×
210
        };
211

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

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

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

230
    fn spatial_partition_to_read_window(
845✔
231
        &self,
845✔
232
        spatial_partition: &SpatialPartition2D,
845✔
233
    ) -> GdalReadWindow {
845✔
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 =
845✔
237
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
845✔
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() {
845✔
270
            (
843✔
271
                spatial_partition.upper_left(),
843✔
272
                spatial_partition.lower_right(),
843✔
273
            )
843✔
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;
845✔
283
        // Move the coordinate far from the origin a bit inside the bbox by subtracting an epsilon of the pixel size
845✔
284
        let safe_far_coord = far_origin_coord - epsilon;
845✔
285

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

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

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

845✔
295
        GdalReadWindow {
845✔
296
            start_x: near_idx_x,
845✔
297
            start_y: near_idx_y,
845✔
298
            size_x: read_size_x,
845✔
299
            size_y: read_size_y,
845✔
300
        }
845✔
301
    }
845✔
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
843✔
319
    where
843✔
320
        M: Into<Self::Margin>,
843✔
321
    {
843✔
322
        let m = margin.into();
843✔
323
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
843✔
324
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
842✔
325
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
842✔
326
    }
843✔
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,035✔
349
        Self {
1,035✔
350
            origin_coordinate: (gdal_geo_transform[0], gdal_geo_transform[3]).into(),
1,035✔
351
            x_pixel_size: gdal_geo_transform[1],
1,035✔
352
            y_pixel_size: gdal_geo_transform[5],
1,035✔
353
        }
1,035✔
354
    }
1,035✔
355
}
356

357
impl SpatialPartitioned for GdalDatasetParameters {
358
    fn spatial_partition(&self) -> SpatialPartition2D {
870✔
359
        self.geo_transform
870✔
360
            .spatial_partition(self.width, self.height)
870✔
361
    }
870✔
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
373
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, FromSql, ToSql)]
2,666✔
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>(
836✔
439
        dataset_params: GdalDatasetParameters,
836✔
440
        tile_information: TileInformation,
836✔
441
        tile_time: TimeInterval,
836✔
442
        cache_hint: CacheHint,
836✔
443
    ) -> Result<RasterTile2D<T>> {
836✔
444
        // TODO: detect usage of vsi curl properly, e.g. also check for `/vsicurl_streaming` and combinations with `/vsizip`
836✔
445
        let is_vsi_curl = dataset_params.file_path.starts_with("/vsicurl/");
836✔
446

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

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

466
                    match load_tile_result {
819✔
467
                        Ok(Ok(r)) => Ok(r),
813✔
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
                }
819✔
479
            },
842✔
480
        )
836✔
481
        .await
815✔
482
    }
813✔
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 {
870✔
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)
836✔
493
                if tile_information
870✔
494
                    .spatial_partition()
870✔
495
                    .intersects(&ds.spatial_partition()) =>
870✔
496
            {
836✔
497
                debug!(
836✔
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
836✔
502
            }
503
            Some(_) => {
504
                debug!("Skipping tile not in query rect {:?}", &tile_information);
34✔
505

506
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
34✔
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
    }
893✔
519

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

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

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

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

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

557
        if let Err(error) = &dataset_result {
851✔
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
        };
844✔
578

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

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

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

592
        Ok(result_tile)
841✔
593
    }
851✔
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✔
UNCOV
672
            crate::error::GdalSourceDoesNotSupportQueryingOtherBandsThanTheFirstOneYet
×
673
        );
674

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

682
        debug!(
120✔
UNCOV
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) => {
UNCOV
749
                log::warn!("The provider did not provide a time range that covers the query. Falling back to query time range. ");
×
UNCOV
750
                FillerTimeBounds::new(query.time_interval.start(), query.time_interval.end())
×
751
            }
UNCOV
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. ");
×
UNCOV
754
                FillerTimeBounds::new(start, query.time_interval.end())
×
755
            }
UNCOV
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. ");
×
UNCOV
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 {
266✔
790
        &self.result_descriptor
266✔
791
    }
266✔
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]
38✔
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>> {
59✔
808
        let data_id = context.resolve_named_data(&self.params.data).await?;
152✔
809
        let meta_data: GdalMetaData = context.meta_data(&data_id).await?;
877✔
810

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

814
        let op = InitializedGdalSourceOperator {
58✔
815
            name: CanonicOperatorName::from(&self),
58✔
816
            result_descriptor: meta_data.result_descriptor().await?,
58✔
817
            meta_data,
58✔
818
            tiling_specification: context.tiling_specification(),
58✔
819
        };
58✔
820

58✔
821
        Ok(op.boxed())
58✔
822
    }
118✔
823

824
    span_fn!(GdalSource);
825
}
826

827
pub struct InitializedGdalSourceOperator {
828
    name: CanonicOperatorName,
829
    pub meta_data: GdalMetaData,
830
    pub result_descriptor: RasterResultDescriptor,
831
    pub tiling_specification: TilingSpecification,
832
}
833

834
impl InitializedRasterOperator for InitializedGdalSourceOperator {
835
    fn result_descriptor(&self) -> &RasterResultDescriptor {
97✔
836
        &self.result_descriptor
97✔
837
    }
97✔
838

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

922
    fn canonic_name(&self) -> CanonicOperatorName {
1✔
923
        self.name.clone()
1✔
924
    }
1✔
925
}
926

927
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
928
/// It fails if the tile is not within the dataset.
929
#[allow(clippy::float_cmp)]
930
fn read_grid_from_raster<T, D>(
843✔
931
    rasterband: &GdalRasterBand,
843✔
932
    read_window: &GdalReadWindow,
843✔
933
    out_shape: D,
843✔
934
    dataset_params: &GdalDatasetParameters,
843✔
935
    flip_y_axis: bool,
843✔
936
) -> Result<GridOrEmpty<D, T>>
843✔
937
where
843✔
938
    T: Pixel + GdalType + Default + FromPrimitive,
843✔
939
    D: Clone + GridSize + PartialEq,
843✔
940
{
843✔
941
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
843✔
942

943
    let buffer = rasterband.read_as::<T>(
843✔
944
        read_window.gdal_window_start(), // pixelspace origin
843✔
945
        read_window.gdal_window_size(),  // pixelspace size
843✔
946
        gdal_out_shape,                  // requested raster size
843✔
947
        None,                            // sampling mode
843✔
948
    )?;
843✔
949
    let (_, buffer_data) = buffer.into_shape_and_vec();
841✔
950
    let data_grid = Grid::new(out_shape.clone(), buffer_data)?;
841✔
951

952
    let data_grid = if flip_y_axis {
841✔
953
        data_grid.reversed_y_axis_grid()
1✔
954
    } else {
955
        data_grid
840✔
956
    };
957

958
    let dataset_mask_flags = rasterband.mask_flags()?;
841✔
959

960
    if dataset_mask_flags.is_all_valid() {
841✔
961
        debug!("all pixels are valid --> skip no-data and mask handling.");
16✔
962
        return Ok(MaskedGrid::new_with_data(data_grid).into());
16✔
963
    }
825✔
964

825✔
965
    if dataset_mask_flags.is_nodata() {
825✔
966
        debug!("raster uses a no-data value --> use no-data handling.");
816✔
967
        let no_data_value = dataset_params
816✔
968
            .no_data_value
816✔
969
            .or_else(|| rasterband.no_data_value())
816✔
970
            .and_then(FromPrimitive::from_f64);
816✔
971
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
816✔
972
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
816✔
973
        return Ok(grid_or_empty);
816✔
974
    }
9✔
975

9✔
976
    if dataset_mask_flags.is_alpha() {
9✔
977
        debug!("raster uses alpha band to mask pixels.");
×
978
        if !dataset_params.allow_alphaband_as_mask {
×
979
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
980
        }
×
981
    }
9✔
982

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

985
    let mask_band = rasterband.open_mask_band()?;
9✔
986
    let mask_buffer = mask_band.read_as::<u8>(
9✔
987
        read_window.gdal_window_start(), // pixelspace origin
9✔
988
        read_window.gdal_window_size(),  // pixelspace size
9✔
989
        gdal_out_shape,                  // requested raster size
9✔
990
        None,                            // sampling mode
9✔
991
    )?;
9✔
992
    let (_, mask_buffer_data) = mask_buffer.into_shape_and_vec();
9✔
993
    let mask_grid = Grid::new(out_shape, mask_buffer_data)?.map_elements(|p: u8| p > 0);
360,020✔
994

995
    let mask_grid = if flip_y_axis {
9✔
996
        mask_grid.reversed_y_axis_grid()
×
997
    } else {
998
        mask_grid
9✔
999
    };
1000

1001
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
9✔
1002
    Ok(GridOrEmpty::from(masked_grid))
9✔
1003
}
843✔
1004

1005
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
1006
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
1007
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
1008
fn read_partial_grid_from_raster<T>(
498✔
1009
    rasterband: &GdalRasterBand,
498✔
1010
    gdal_read_window: &GdalReadWindow,
498✔
1011
    out_tile_read_bounds: GridBoundingBox2D,
498✔
1012
    out_tile_shape: GridShape2D,
498✔
1013
    dataset_params: &GdalDatasetParameters,
498✔
1014
    flip_y_axis: bool,
498✔
1015
) -> Result<GridOrEmpty2D<T>>
498✔
1016
where
498✔
1017
    T: Pixel + GdalType + Default + FromPrimitive,
498✔
1018
{
498✔
1019
    let dataset_raster = read_grid_from_raster(
498✔
1020
        rasterband,
498✔
1021
        gdal_read_window,
498✔
1022
        out_tile_read_bounds,
498✔
1023
        dataset_params,
498✔
1024
        flip_y_axis,
498✔
1025
    )?;
498✔
1026

1027
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
496✔
1028
    tile_raster.grid_blit_from(&dataset_raster);
496✔
1029
    Ok(tile_raster)
496✔
1030
}
498✔
1031

1032
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
1033
/// It handles conversion to grid coordinates.
1034
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
1035
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
1036
fn read_grid_and_handle_edges<T>(
843✔
1037
    tile_info: TileInformation,
843✔
1038
    dataset: &GdalDataset,
843✔
1039
    rasterband: &GdalRasterBand,
843✔
1040
    dataset_params: &GdalDatasetParameters,
843✔
1041
) -> Result<GridOrEmpty2D<T>>
843✔
1042
where
843✔
1043
    T: Pixel + GdalType + Default + FromPrimitive,
843✔
1044
{
843✔
1045
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
843✔
1046
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
843✔
1047

843✔
1048
    if !approx_eq!(
843✔
1049
        GdalDatasetGeoTransform,
843✔
1050
        gdal_dataset_geotransform,
843✔
1051
        dataset_params.geo_transform
843✔
1052
    ) {
843✔
1053
        log::warn!(
1✔
1054
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
1055
            dataset_params.geo_transform,
1056
            gdal_dataset_geotransform,
1057
        );
1058
    };
842✔
1059

1060
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
843✔
1061
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
843✔
1062

1063
    let gdal_dataset_bounds =
843✔
1064
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
843✔
1065

843✔
1066
    let output_bounds = tile_info.spatial_partition();
843✔
1067
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
843✔
1068
    let output_shape = tile_info.tile_size_in_pixels();
843✔
1069

1070
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
843✔
1071
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
1072
    };
1073

1074
    let tile_geo_transform = tile_info.tile_geo_transform();
843✔
1075

843✔
1076
    let gdal_read_window =
843✔
1077
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
843✔
1078

843✔
1079
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
843✔
1080
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
843✔
1081

843✔
1082
    if is_y_axis_flipped {
843✔
1083
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1084
    }
842✔
1085

1086
    let result_grid = if dataset_intersection_area == output_bounds {
843✔
1087
        read_grid_from_raster(
345✔
1088
            rasterband,
345✔
1089
            &gdal_read_window,
345✔
1090
            output_shape,
345✔
1091
            dataset_params,
345✔
1092
            is_y_axis_flipped,
345✔
1093
        )?
345✔
1094
    } else {
1095
        let partial_tile_grid_bounds =
498✔
1096
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
498✔
1097

498✔
1098
        read_partial_grid_from_raster(
498✔
1099
            rasterband,
498✔
1100
            &gdal_read_window,
498✔
1101
            partial_tile_grid_bounds,
498✔
1102
            output_shape,
498✔
1103
            dataset_params,
498✔
1104
            is_y_axis_flipped,
498✔
1105
        )?
498✔
1106
    };
1107

1108
    Ok(result_grid)
841✔
1109
}
843✔
1110

1111
/// 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.
1112
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
844✔
1113
    dataset: &GdalDataset,
844✔
1114
    dataset_params: &GdalDatasetParameters,
844✔
1115
    tile_info: TileInformation,
844✔
1116
    tile_time: TimeInterval,
844✔
1117
    cache_hint: CacheHint,
844✔
1118
) -> Result<RasterTile2D<T>> {
844✔
1119
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel)?;
844✔
1120

1121
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
843✔
1122

1123
    let mut properties = RasterProperties::default();
841✔
1124

841✔
1125
    // always read the scale and offset values from the rasterband
841✔
1126
    properties_from_band(&mut properties, &rasterband);
841✔
1127

1128
    // read the properties from the dataset and rasterband metadata
1129
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
841✔
1130
        properties_from_gdal_metadata(&mut properties, dataset, properties_mapping);
4✔
1131
        properties_from_gdal_metadata(&mut properties, &rasterband, properties_mapping);
4✔
1132
    }
837✔
1133

1134
    // TODO: add cache_hint
1135
    Ok(RasterTile2D::new_with_tile_info_and_properties(
841✔
1136
        tile_time,
841✔
1137
        tile_info,
841✔
1138
        0,
841✔
1139
        result_grid,
841✔
1140
        properties,
841✔
1141
        cache_hint,
841✔
1142
    ))
841✔
1143
}
844✔
1144

1145
fn create_no_data_tile<T: Pixel>(
82✔
1146
    tile_info: TileInformation,
82✔
1147
    tile_time: TimeInterval,
82✔
1148
    cache_hint: CacheHint,
82✔
1149
) -> RasterTile2D<T> {
82✔
1150
    // TODO: add cache_hint
82✔
1151
    RasterTile2D::new_with_tile_info_and_properties(
82✔
1152
        tile_time,
82✔
1153
        tile_info,
82✔
1154
        0,
82✔
1155
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
82✔
1156
        RasterProperties::default(),
82✔
1157
        cache_hint,
82✔
1158
    )
82✔
1159
}
82✔
1160

1161
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
3,486✔
1162
pub struct GdalMetadataMapping {
1163
    pub source_key: RasterPropertiesKey,
1164
    pub target_key: RasterPropertiesKey,
1165
    pub target_type: RasterPropertiesEntryType,
1166
}
1167

1168
impl GdalMetadataMapping {
1169
    pub fn identity(
×
1170
        key: RasterPropertiesKey,
×
1171
        target_type: RasterPropertiesEntryType,
×
1172
    ) -> GdalMetadataMapping {
×
1173
        GdalMetadataMapping {
×
1174
            source_key: key.clone(),
×
1175
            target_key: key,
×
1176
            target_type,
×
1177
        }
×
1178
    }
×
1179
}
1180

1181
fn properties_from_gdal_metadata<'a, I, M>(
8✔
1182
    properties: &mut RasterProperties,
8✔
1183
    gdal_dataset: &M,
8✔
1184
    properties_mapping: I,
8✔
1185
) where
8✔
1186
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1187
    M: GdalMetadata,
8✔
1188
{
8✔
1189
    let mapping_iter = properties_mapping.into_iter();
8✔
1190

1191
    for m in mapping_iter {
24✔
1192
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1193
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1194
        } else {
1195
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1196
        };
1197

1198
        if let Some(d) = data {
16✔
1199
            let entry = match m.target_type {
8✔
1200
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1201
                    |_| RasterPropertiesEntry::String(d),
×
1202
                    RasterPropertiesEntry::Number,
×
1203
                ),
×
1204
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1205
            };
1206

1207
            debug!(
8✔
1208
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1209
                &m.source_key, &m.target_key, &entry
×
1210
            );
1211

1212
            properties.insert_property(m.target_key.clone(), entry);
8✔
1213
        }
8✔
1214
    }
1215
}
8✔
1216

1217
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
841✔
1218
    if let Some(scale) = gdal_dataset.scale() {
841✔
1219
        properties.set_scale(scale);
1✔
1220
    };
840✔
1221
    if let Some(offset) = gdal_dataset.offset() {
841✔
1222
        properties.set_offset(offset);
1✔
1223
    };
840✔
1224

1225
    // ignore if there is no description
1226
    if let Ok(description) = gdal_dataset.description() {
841✔
1227
        properties.set_description(description);
841✔
1228
    }
841✔
1229
}
841✔
1230

1231
#[cfg(test)]
1232
mod tests {
1233
    use super::*;
1234
    use crate::engine::{MockExecutionContext, MockQueryContext};
1235
    use crate::test_data;
1236
    use crate::util::gdal::add_ndvi_dataset;
1237
    use crate::util::Result;
1238
    use geoengine_datatypes::hashmap;
1239
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1240
    use geoengine_datatypes::raster::{
1241
        EmptyGrid2D, GridBounds, GridIdx2D, TilesEqualIgnoringCacheHint,
1242
    };
1243
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1244
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1245
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1246
    use httptest::matchers::request;
1247
    use httptest::{responders, Expectation, Server};
1248

1249
    async fn query_gdal_source(
5✔
1250
        exe_ctx: &MockExecutionContext,
5✔
1251
        query_ctx: &MockQueryContext,
5✔
1252
        name: NamedData,
5✔
1253
        output_shape: GridShape2D,
5✔
1254
        output_bounds: SpatialPartition2D,
5✔
1255
        time_interval: TimeInterval,
5✔
1256
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1257
        let op = GdalSource {
5✔
1258
            params: GdalSourceParameters { data: name.clone() },
5✔
1259
        }
5✔
1260
        .boxed();
5✔
1261

5✔
1262
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1263
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1264
        let spatial_resolution =
5✔
1265
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1266

1267
        let o = op
5✔
1268
            .initialize(WorkflowOperatorPath::initialize_root(), exe_ctx)
5✔
1269
            .await
×
1270
            .unwrap();
5✔
1271

5✔
1272
        o.query_processor()
5✔
1273
            .unwrap()
5✔
1274
            .get_u8()
5✔
1275
            .unwrap()
5✔
1276
            .raster_query(
5✔
1277
                RasterQueryRectangle {
5✔
1278
                    spatial_bounds: output_bounds,
5✔
1279
                    time_interval,
5✔
1280
                    spatial_resolution,
5✔
1281
                    attributes: BandSelection::first(),
5✔
1282
                },
5✔
1283
                query_ctx,
5✔
1284
            )
5✔
1285
            .await
×
1286
            .unwrap()
5✔
1287
            .collect()
5✔
1288
            .await
14✔
1289
    }
5✔
1290

1291
    fn load_ndvi_jan_2014(
3✔
1292
        output_shape: GridShape2D,
3✔
1293
        output_bounds: SpatialPartition2D,
3✔
1294
    ) -> Result<RasterTile2D<u8>> {
3✔
1295
        GdalRasterLoader::load_tile_data::<u8>(
3✔
1296
            &GdalDatasetParameters {
3✔
1297
                file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
3✔
1298
                rasterband_channel: 1,
3✔
1299
                geo_transform: GdalDatasetGeoTransform {
3✔
1300
                    origin_coordinate: (-180., 90.).into(),
3✔
1301
                    x_pixel_size: 0.1,
3✔
1302
                    y_pixel_size: -0.1,
3✔
1303
                },
3✔
1304
                width: 3600,
3✔
1305
                height: 1800,
3✔
1306
                file_not_found_handling: FileNotFoundHandling::NoData,
3✔
1307
                no_data_value: Some(0.),
3✔
1308
                properties_mapping: Some(vec![
3✔
1309
                    GdalMetadataMapping {
3✔
1310
                        source_key: RasterPropertiesKey {
3✔
1311
                            domain: None,
3✔
1312
                            key: "AREA_OR_POINT".to_string(),
3✔
1313
                        },
3✔
1314
                        target_type: RasterPropertiesEntryType::String,
3✔
1315
                        target_key: RasterPropertiesKey {
3✔
1316
                            domain: None,
3✔
1317
                            key: "AREA_OR_POINT".to_string(),
3✔
1318
                        },
3✔
1319
                    },
3✔
1320
                    GdalMetadataMapping {
3✔
1321
                        source_key: RasterPropertiesKey {
3✔
1322
                            domain: Some("IMAGE_STRUCTURE".to_string()),
3✔
1323
                            key: "COMPRESSION".to_string(),
3✔
1324
                        },
3✔
1325
                        target_type: RasterPropertiesEntryType::String,
3✔
1326
                        target_key: RasterPropertiesKey {
3✔
1327
                            domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
3✔
1328
                            key: "COMPRESSION".to_string(),
3✔
1329
                        },
3✔
1330
                    },
3✔
1331
                ]),
3✔
1332
                gdal_open_options: None,
3✔
1333
                gdal_config_options: None,
3✔
1334
                allow_alphaband_as_mask: true,
3✔
1335
                retry: None,
3✔
1336
            },
3✔
1337
            TileInformation::with_partition_and_shape(output_bounds, output_shape),
3✔
1338
            TimeInterval::default(),
3✔
1339
            CacheHint::default(),
3✔
1340
        )
3✔
1341
    }
3✔
1342

1343
    #[test]
1344
    fn tiling_strategy_origin() {
1✔
1345
        let tile_size_in_pixels = [600, 600];
1✔
1346
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1347
        let dataset_x_pixel_size = 0.1;
1✔
1348
        let dataset_y_pixel_size = -0.1;
1✔
1349
        let dataset_geo_transform = GeoTransform::new(
1✔
1350
            dataset_upper_right_coord,
1✔
1351
            dataset_x_pixel_size,
1✔
1352
            dataset_y_pixel_size,
1✔
1353
        );
1✔
1354

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

1✔
1357
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1358
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1359
            geo_transform: dataset_geo_transform,
1✔
1360
        };
1✔
1361

1✔
1362
        assert_eq!(
1✔
1363
            origin_split_tileing_strategy
1✔
1364
                .geo_transform
1✔
1365
                .upper_left_pixel_idx(&partition),
1✔
1366
            [0, 0].into()
1✔
1367
        );
1✔
1368
        assert_eq!(
1✔
1369
            origin_split_tileing_strategy
1✔
1370
                .geo_transform
1✔
1371
                .lower_right_pixel_idx(&partition),
1✔
1372
            [1800 - 1, 3600 - 1].into()
1✔
1373
        );
1✔
1374

1375
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1376
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1377
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1378
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1379
    }
1✔
1380

1381
    #[test]
1382
    fn tiling_strategy_zero() {
1✔
1383
        let tile_size_in_pixels = [600, 600];
1✔
1384
        let dataset_x_pixel_size = 0.1;
1✔
1385
        let dataset_y_pixel_size = -0.1;
1✔
1386
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1387
            0.0,
1✔
1388
            dataset_x_pixel_size,
1✔
1389
            0.0,
1✔
1390
            dataset_y_pixel_size,
1✔
1391
        );
1✔
1392

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

1✔
1395
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1396
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1397
            geo_transform: central_geo_transform,
1✔
1398
        };
1✔
1399

1✔
1400
        assert_eq!(
1✔
1401
            origin_split_tileing_strategy
1✔
1402
                .geo_transform
1✔
1403
                .upper_left_pixel_idx(&partition),
1✔
1404
            [-900, -1800].into()
1✔
1405
        );
1✔
1406
        assert_eq!(
1✔
1407
            origin_split_tileing_strategy
1✔
1408
                .geo_transform
1✔
1409
                .lower_right_pixel_idx(&partition),
1✔
1410
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1411
        );
1✔
1412

1413
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1414
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1415
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1416
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1417
    }
1✔
1418

1419
    #[test]
1420
    fn tile_idx_iterator() {
1✔
1421
        let tile_size_in_pixels = [600, 600];
1✔
1422
        let dataset_x_pixel_size = 0.1;
1✔
1423
        let dataset_y_pixel_size = -0.1;
1✔
1424
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1425
            0.0,
1✔
1426
            dataset_x_pixel_size,
1✔
1427
            0.0,
1✔
1428
            dataset_y_pixel_size,
1✔
1429
        );
1✔
1430

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

1✔
1433
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1434
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1435
            geo_transform: central_geo_transform,
1✔
1436
        };
1✔
1437

1✔
1438
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1439
            .tile_idx_iterator(partition)
1✔
1440
            .collect();
1✔
1441
        assert_eq!(vres.len(), 4 * 6);
1✔
1442
        assert_eq!(vres[0], [-2, -3].into());
1✔
1443
        assert_eq!(vres[1], [-2, -2].into());
1✔
1444
        assert_eq!(vres[2], [-2, -1].into());
1✔
1445
        assert_eq!(vres[23], [1, 2].into());
1✔
1446
    }
1✔
1447

1448
    #[test]
1449
    fn tile_information_iterator() {
1✔
1450
        let tile_size_in_pixels = [600, 600];
1✔
1451
        let dataset_x_pixel_size = 0.1;
1✔
1452
        let dataset_y_pixel_size = -0.1;
1✔
1453

1✔
1454
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1455
            0.0,
1✔
1456
            dataset_x_pixel_size,
1✔
1457
            0.0,
1✔
1458
            dataset_y_pixel_size,
1✔
1459
        );
1✔
1460

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

1✔
1463
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1464
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1465
            geo_transform: central_geo_transform,
1✔
1466
        };
1✔
1467

1✔
1468
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1469
            .tile_information_iterator(partition)
1✔
1470
            .collect();
1✔
1471
        assert_eq!(vres.len(), 4 * 6);
1✔
1472
        assert_eq!(
1✔
1473
            vres[0],
1✔
1474
            TileInformation::new(
1✔
1475
                [-2, -3].into(),
1✔
1476
                tile_size_in_pixels.into(),
1✔
1477
                central_geo_transform,
1✔
1478
            )
1✔
1479
        );
1✔
1480
        assert_eq!(
1✔
1481
            vres[1],
1✔
1482
            TileInformation::new(
1✔
1483
                [-2, -2].into(),
1✔
1484
                tile_size_in_pixels.into(),
1✔
1485
                central_geo_transform,
1✔
1486
            )
1✔
1487
        );
1✔
1488
        assert_eq!(
1✔
1489
            vres[12],
1✔
1490
            TileInformation::new(
1✔
1491
                [0, -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[23],
1✔
1498
            TileInformation::new(
1✔
1499
                [1, 2].into(),
1✔
1500
                tile_size_in_pixels.into(),
1✔
1501
                central_geo_transform,
1✔
1502
            )
1✔
1503
        );
1✔
1504
    }
1✔
1505

1506
    #[test]
1507
    fn replace_time_placeholder() {
1✔
1508
        let params = GdalDatasetParameters {
1✔
1509
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1510
            rasterband_channel: 0,
1✔
1511
            geo_transform: TestDefault::test_default(),
1✔
1512
            width: 360,
1✔
1513
            height: 180,
1✔
1514
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1515
            no_data_value: Some(0.),
1✔
1516
            properties_mapping: None,
1✔
1517
            gdal_open_options: None,
1✔
1518
            gdal_config_options: None,
1✔
1519
            allow_alphaband_as_mask: true,
1✔
1520
            retry: None,
1✔
1521
        };
1✔
1522
        let replaced = params
1✔
1523
            .replace_time_placeholders(
1✔
1524
                &hashmap! {
1✔
1525
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1526
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1527
                        reference: TimeReference::Start,
1✔
1528
                    },
1✔
1529
                },
1✔
1530
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1531
            )
1✔
1532
            .unwrap();
1✔
1533
        assert_eq!(
1✔
1534
            replaced.file_path.to_string_lossy(),
1✔
1535
            "/foo/bar_022000000.tiff".to_string()
1✔
1536
        );
1✔
1537
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1538
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1539
        assert_eq!(params.width, replaced.width);
1✔
1540
        assert_eq!(params.height, replaced.height);
1✔
1541
        assert_eq!(
1✔
1542
            params.file_not_found_handling,
1✔
1543
            replaced.file_not_found_handling
1✔
1544
        );
1✔
1545
    }
1✔
1546

1547
    #[test]
1548
    fn test_load_tile_data() {
1✔
1549
        let output_shape: GridShape2D = [8, 8].into();
1✔
1550
        let output_bounds =
1✔
1551
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1552

1✔
1553
        let RasterTile2D {
1✔
1554
            global_geo_transform: _,
1✔
1555
            grid_array: grid,
1✔
1556
            tile_position: _,
1✔
1557
            band: _,
1✔
1558
            time: _,
1✔
1559
            properties,
1✔
1560
            cache_hint: _,
1✔
1561
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1562

1✔
1563
        assert!(!grid.is_empty());
1✔
1564

1565
        let grid = grid.into_materialized_masked_grid();
1✔
1566

1✔
1567
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1568
        assert_eq!(
1✔
1569
            grid.inner_grid.data,
1✔
1570
            &[
1✔
1571
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1572
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1573
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1574
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1575
            ]
1✔
1576
        );
1✔
1577

1578
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1579
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1580

1581
        assert!((properties.scale_option()).is_none());
1✔
1582
        assert!(properties.offset_option().is_none());
1✔
1583
        assert_eq!(
1✔
1584
            properties.get_property(&RasterPropertiesKey {
1✔
1585
                key: "AREA_OR_POINT".to_string(),
1✔
1586
                domain: None,
1✔
1587
            }),
1✔
1588
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1589
        );
1✔
1590
        assert_eq!(
1✔
1591
            properties.get_property(&RasterPropertiesKey {
1✔
1592
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1593
                key: "COMPRESSION".to_string(),
1✔
1594
            }),
1✔
1595
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1596
        );
1✔
1597
    }
1✔
1598

1599
    #[test]
1600
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1601
        let output_shape: GridShape2D = [8, 8].into();
1✔
1602
        // shift world bbox one pixel up and to the left
1✔
1603
        let (x_size, y_size) = (45., 22.5);
1✔
1604
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1605
            (-180. - x_size, 90. + y_size).into(),
1✔
1606
            (180. - x_size, -90. + y_size).into(),
1✔
1607
        );
1✔
1608

1✔
1609
        let RasterTile2D {
1✔
1610
            global_geo_transform: _,
1✔
1611
            grid_array: grid,
1✔
1612
            tile_position: _,
1✔
1613
            band: _,
1✔
1614
            time: _,
1✔
1615
            properties: _,
1✔
1616
            cache_hint: _,
1✔
1617
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1618

1✔
1619
        assert!(!grid.is_empty());
1✔
1620

1621
        let x = grid.into_materialized_masked_grid();
1✔
1622

1✔
1623
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1624
        assert_eq!(
1✔
1625
            x.inner_grid.data,
1✔
1626
            &[
1✔
1627
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1628
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1629
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1630
                255, 255, 255, 255, 255, 255
1✔
1631
            ]
1✔
1632
        );
1✔
1633
    }
1✔
1634

1635
    #[test]
1636
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1637
        let output_shape: GridShape2D = [8, 8].into();
1✔
1638
        // shift world bbox one pixel up and to the left
1✔
1639
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1640
        let output_bounds = SpatialPartition2D::new(
1✔
1641
            (-116.22222, 66.66666).into(),
1✔
1642
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1643
        )
1✔
1644
        .unwrap();
1✔
1645

1✔
1646
        let RasterTile2D {
1✔
1647
            global_geo_transform: _,
1✔
1648
            grid_array: grid,
1✔
1649
            tile_position: _,
1✔
1650
            band: _,
1✔
1651
            time: _,
1✔
1652
            properties: _,
1✔
1653
            cache_hint: _,
1✔
1654
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1655

1✔
1656
        assert!(!grid.is_empty());
1✔
1657

1658
        let x = grid.into_materialized_masked_grid();
1✔
1659

1✔
1660
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1661
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1662
    }
1✔
1663

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

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

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

1✔
1686
        assert_eq!(c.len(), 4);
1✔
1687

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

1✔
1693
        assert_eq!(
1✔
1694
            c[0].tile_information().global_tile_position(),
1✔
1695
            [-1, -1].into()
1✔
1696
        );
1✔
1697

1✔
1698
        assert_eq!(
1✔
1699
            c[1].tile_information().global_tile_position(),
1✔
1700
            [-1, 0].into()
1✔
1701
        );
1✔
1702

1✔
1703
        assert_eq!(
1✔
1704
            c[2].tile_information().global_tile_position(),
1✔
1705
            [0, -1].into()
1✔
1706
        );
1✔
1707

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

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

1✔
1720
        let output_shape: GridShape2D = [256, 256].into();
1✔
1721
        let output_bounds =
1✔
1722
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1723
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-01
1✔
1724

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

1✔
1736
        assert_eq!(c.len(), 8);
1✔
1737

1✔
1738
        assert_eq!(
1✔
1739
            c[0].time,
1✔
1740
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1741
        );
1✔
1742

1✔
1743
        assert_eq!(
1✔
1744
            c[5].time,
1✔
1745
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1746
        );
1✔
1747
    }
1✔
1748

1749
    #[tokio::test]
1750
    async fn test_query_before_data() {
1✔
1751
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1752
        let query_ctx = MockQueryContext::test_default();
1✔
1753
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1754

1✔
1755
        let output_shape: GridShape2D = [256, 256].into();
1✔
1756
        let output_bounds =
1✔
1757
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1758
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1759

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

1✔
1771
        assert_eq!(c.len(), 4);
1✔
1772

1✔
1773
        assert_eq!(
1✔
1774
            c[0].time,
1✔
1775
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1776
        );
1✔
1777
    }
1✔
1778

1779
    #[tokio::test]
1780
    async fn test_query_after_data() {
1✔
1781
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1782
        let query_ctx = MockQueryContext::test_default();
1✔
1783
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1784

1✔
1785
        let output_shape: GridShape2D = [256, 256].into();
1✔
1786
        let output_bounds =
1✔
1787
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1788
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1789

1✔
1790
        let c = query_gdal_source(
1✔
1791
            &exe_ctx,
1✔
1792
            &query_ctx,
1✔
1793
            id,
1✔
1794
            output_shape,
1✔
1795
            output_bounds,
1✔
1796
            time_interval,
1✔
1797
        )
1✔
1798
        .await;
1✔
1799
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1800

1✔
1801
        assert_eq!(c.len(), 4);
1✔
1802

1✔
1803
        assert_eq!(
1✔
1804
            c[0].time,
1✔
1805
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1806
        );
1✔
1807
    }
1✔
1808

1809
    #[tokio::test]
1810
    async fn test_nodata() {
1✔
1811
        hide_gdal_errors();
1✔
1812

1✔
1813
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1814
        let query_ctx = MockQueryContext::test_default();
1✔
1815
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1816

1✔
1817
        let output_shape: GridShape2D = [256, 256].into();
1✔
1818
        let output_bounds =
1✔
1819
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1820
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1821

1✔
1822
        let c = query_gdal_source(
1✔
1823
            &exe_ctx,
1✔
1824
            &query_ctx,
1✔
1825
            id,
1✔
1826
            output_shape,
1✔
1827
            output_bounds,
1✔
1828
            time_interval,
1✔
1829
        )
1✔
1830
        .await;
1✔
1831
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1832

1✔
1833
        assert_eq!(c.len(), 4);
1✔
1834

1✔
1835
        let tile_1 = &c[0];
1✔
1836

1✔
1837
        assert_eq!(
1✔
1838
            tile_1.time,
1✔
1839
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1840
        );
1✔
1841

1✔
1842
        assert!(tile_1.is_empty());
1✔
1843
    }
1✔
1844

1845
    #[tokio::test]
1846
    async fn timestep_without_params() {
1✔
1847
        let output_bounds =
1✔
1848
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1849
        let output_shape: GridShape2D = [256, 256].into();
1✔
1850

1✔
1851
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1852
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1853
        let params = None;
1✔
1854

1✔
1855
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
1856
            params,
1✔
1857
            tile_info,
1✔
1858
            time_interval,
1✔
1859
            CacheHint::default(),
1✔
1860
        )
1✔
1861
        .await;
1✔
1862

1✔
1863
        assert!(tile.is_ok());
1✔
1864

1✔
1865
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1866
            time_interval,
1✔
1867
            tile_info,
1✔
1868
            0,
1✔
1869
            EmptyGrid2D::new(output_shape).into(),
1✔
1870
            CacheHint::default(),
1✔
1871
        );
1✔
1872

1✔
1873
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
1874
    }
1✔
1875

1876
    #[test]
1877
    fn it_reverts_config_options() {
1✔
1878
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1879

1✔
1880
        {
1✔
1881
            let _config =
1✔
1882
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1883

1✔
1884
            assert_eq!(
1✔
1885
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1886
                "bar".to_owned()
1✔
1887
            );
1✔
1888
        }
1889

1890
        assert_eq!(
1✔
1891
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1892
            String::new()
1✔
1893
        );
1✔
1894
    }
1✔
1895

1896
    #[test]
1897
    #[allow(clippy::too_many_lines)]
1898
    fn deserialize_dataset_parameters() {
1✔
1899
        let dataset_parameters = GdalDatasetParameters {
1✔
1900
            file_path: "path-to-data.tiff".into(),
1✔
1901
            rasterband_channel: 1,
1✔
1902
            geo_transform: GdalDatasetGeoTransform {
1✔
1903
                origin_coordinate: (-180., 90.).into(),
1✔
1904
                x_pixel_size: 0.1,
1✔
1905
                y_pixel_size: -0.1,
1✔
1906
            },
1✔
1907
            width: 3600,
1✔
1908
            height: 1800,
1✔
1909
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1910
            no_data_value: Some(f64::NAN),
1✔
1911
            properties_mapping: Some(vec![
1✔
1912
                GdalMetadataMapping {
1✔
1913
                    source_key: RasterPropertiesKey {
1✔
1914
                        domain: None,
1✔
1915
                        key: "AREA_OR_POINT".to_string(),
1✔
1916
                    },
1✔
1917
                    target_type: RasterPropertiesEntryType::String,
1✔
1918
                    target_key: RasterPropertiesKey {
1✔
1919
                        domain: None,
1✔
1920
                        key: "AREA_OR_POINT".to_string(),
1✔
1921
                    },
1✔
1922
                },
1✔
1923
                GdalMetadataMapping {
1✔
1924
                    source_key: RasterPropertiesKey {
1✔
1925
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
1926
                        key: "COMPRESSION".to_string(),
1✔
1927
                    },
1✔
1928
                    target_type: RasterPropertiesEntryType::String,
1✔
1929
                    target_key: RasterPropertiesKey {
1✔
1930
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1931
                        key: "COMPRESSION".to_string(),
1✔
1932
                    },
1✔
1933
                },
1✔
1934
            ]),
1✔
1935
            gdal_open_options: None,
1✔
1936
            gdal_config_options: None,
1✔
1937
            allow_alphaband_as_mask: true,
1✔
1938
            retry: None,
1✔
1939
        };
1✔
1940

1✔
1941
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1942

1✔
1943
        assert_eq!(
1✔
1944
            dataset_parameters_json,
1✔
1945
            serde_json::json!({
1✔
1946
                "filePath": "path-to-data.tiff",
1✔
1947
                "rasterbandChannel": 1,
1✔
1948
                "geoTransform": {
1✔
1949
                    "originCoordinate": {
1✔
1950
                        "x": -180.,
1✔
1951
                        "y": 90.
1✔
1952
                    },
1✔
1953
                    "xPixelSize": 0.1,
1✔
1954
                    "yPixelSize": -0.1
1✔
1955
                },
1✔
1956
                "width": 3600,
1✔
1957
                "height": 1800,
1✔
1958
                "fileNotFoundHandling": "NoData",
1✔
1959
                "noDataValue": "nan",
1✔
1960
                "propertiesMapping": [{
1✔
1961
                        "source_key": {
1✔
1962
                            "domain": null,
1✔
1963
                            "key": "AREA_OR_POINT"
1✔
1964
                        },
1✔
1965
                        "target_key": {
1✔
1966
                            "domain": null,
1✔
1967
                            "key": "AREA_OR_POINT"
1✔
1968
                        },
1✔
1969
                        "target_type": "String"
1✔
1970
                    },
1✔
1971
                    {
1✔
1972
                        "source_key": {
1✔
1973
                            "domain": "IMAGE_STRUCTURE",
1✔
1974
                            "key": "COMPRESSION"
1✔
1975
                        },
1✔
1976
                        "target_key": {
1✔
1977
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1978
                            "key": "COMPRESSION"
1✔
1979
                        },
1✔
1980
                        "target_type": "String"
1✔
1981
                    }
1✔
1982
                ],
1✔
1983
                "gdalOpenOptions": null,
1✔
1984
                "gdalConfigOptions": null,
1✔
1985
                "allowAlphabandAsMask": true,
1✔
1986
                "retry": null,
1✔
1987
            })
1✔
1988
        );
1✔
1989

1990
        let deserialized_parameters =
1✔
1991
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1992

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

1✔
1995
        assert_eq!(
1✔
1996
            deserialized_parameters.file_path,
1✔
1997
            dataset_parameters.file_path,
1✔
1998
        );
1✔
1999
        assert_eq!(
1✔
2000
            deserialized_parameters.rasterband_channel,
1✔
2001
            dataset_parameters.rasterband_channel,
1✔
2002
        );
1✔
2003
        assert_eq!(
1✔
2004
            deserialized_parameters.geo_transform,
1✔
2005
            dataset_parameters.geo_transform,
1✔
2006
        );
1✔
2007
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
2008
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
2009
        assert_eq!(
1✔
2010
            deserialized_parameters.file_not_found_handling,
1✔
2011
            dataset_parameters.file_not_found_handling,
1✔
2012
        );
1✔
2013
        assert!(
1✔
2014
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
2015
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
2016
        );
2017
        assert_eq!(
1✔
2018
            deserialized_parameters.properties_mapping,
1✔
2019
            dataset_parameters.properties_mapping,
1✔
2020
        );
1✔
2021
        assert_eq!(
1✔
2022
            deserialized_parameters.gdal_open_options,
1✔
2023
            dataset_parameters.gdal_open_options,
1✔
2024
        );
1✔
2025
        assert_eq!(
1✔
2026
            deserialized_parameters.gdal_config_options,
1✔
2027
            dataset_parameters.gdal_config_options,
1✔
2028
        );
1✔
2029
    }
1✔
2030

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2095
    #[test]
2096
    fn gdal_read_window_data_origin_upper_left() {
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 = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
2104
            .unwrap();
1✔
2105

1✔
2106
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2107

1✔
2108
        let exp = GdalReadWindow {
1✔
2109
            size_x: 4,
1✔
2110
            size_y: 6,
1✔
2111
            start_x: 6,
1✔
2112
            start_y: 4,
1✔
2113
        };
1✔
2114

1✔
2115
        assert_eq!(rw, exp);
1✔
2116
    }
1✔
2117

2118
    #[test]
2119
    fn gdal_read_window_data_origin_lower_left() {
1✔
2120
        let gt = GdalDatasetGeoTransform {
1✔
2121
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2122
            x_pixel_size: 1.,
1✔
2123
            y_pixel_size: 1.,
1✔
2124
        };
1✔
2125

1✔
2126
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2127
            .unwrap();
1✔
2128

1✔
2129
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2130

1✔
2131
        let exp = GdalReadWindow {
1✔
2132
            size_x: 10,
1✔
2133
            size_y: 10,
1✔
2134
            start_x: 0,
1✔
2135
            start_y: 0,
1✔
2136
        };
1✔
2137

1✔
2138
        assert_eq!(rw, exp);
1✔
2139
    }
1✔
2140

2141
    #[test]
2142
    fn read_up_side_down_raster() {
1✔
2143
        let output_shape: GridShape2D = [8, 8].into();
1✔
2144
        let output_bounds =
1✔
2145
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2146

1✔
2147
        let up_side_down_params = GdalDatasetParameters {
1✔
2148
            file_path: test_data!(
1✔
2149
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2150
            )
1✔
2151
            .into(),
1✔
2152
            rasterband_channel: 1,
1✔
2153
            geo_transform: GdalDatasetGeoTransform {
1✔
2154
                origin_coordinate: (-180., -90.).into(),
1✔
2155
                x_pixel_size: 0.1,
1✔
2156
                y_pixel_size: 0.1,
1✔
2157
            },
1✔
2158
            width: 3600,
1✔
2159
            height: 1800,
1✔
2160
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2161
            no_data_value: Some(0.),
1✔
2162
            properties_mapping: Some(vec![
1✔
2163
                GdalMetadataMapping {
1✔
2164
                    source_key: RasterPropertiesKey {
1✔
2165
                        domain: None,
1✔
2166
                        key: "AREA_OR_POINT".to_string(),
1✔
2167
                    },
1✔
2168
                    target_type: RasterPropertiesEntryType::String,
1✔
2169
                    target_key: RasterPropertiesKey {
1✔
2170
                        domain: None,
1✔
2171
                        key: "AREA_OR_POINT".to_string(),
1✔
2172
                    },
1✔
2173
                },
1✔
2174
                GdalMetadataMapping {
1✔
2175
                    source_key: RasterPropertiesKey {
1✔
2176
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2177
                        key: "COMPRESSION".to_string(),
1✔
2178
                    },
1✔
2179
                    target_type: RasterPropertiesEntryType::String,
1✔
2180
                    target_key: RasterPropertiesKey {
1✔
2181
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2182
                        key: "COMPRESSION".to_string(),
1✔
2183
                    },
1✔
2184
                },
1✔
2185
            ]),
1✔
2186
            gdal_open_options: None,
1✔
2187
            gdal_config_options: None,
1✔
2188
            allow_alphaband_as_mask: true,
1✔
2189
            retry: None,
1✔
2190
        };
1✔
2191

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

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

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

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

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

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

2229
        assert!(properties.offset_option().is_none());
1✔
2230
        assert!(properties.scale_option().is_none());
1✔
2231
    }
1✔
2232

2233
    #[test]
2234
    fn read_raster_and_offset_scale() {
1✔
2235
        let output_shape: GridShape2D = [8, 8].into();
1✔
2236
        let output_bounds =
1✔
2237
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2238

1✔
2239
        let up_side_down_params = GdalDatasetParameters {
1✔
2240
            file_path: test_data!(
1✔
2241
                "raster/modis_ndvi/with_offset_scale/MOD13A2_M_NDVI_2014-01-01.TIFF"
1✔
2242
            )
1✔
2243
            .into(),
1✔
2244
            rasterband_channel: 1,
1✔
2245
            geo_transform: GdalDatasetGeoTransform {
1✔
2246
                origin_coordinate: (-180., -90.).into(),
1✔
2247
                x_pixel_size: 0.1,
1✔
2248
                y_pixel_size: 0.1,
1✔
2249
            },
1✔
2250
            width: 3600,
1✔
2251
            height: 1800,
1✔
2252
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2253
            no_data_value: Some(0.),
1✔
2254
            properties_mapping: None,
1✔
2255
            gdal_open_options: None,
1✔
2256
            gdal_config_options: None,
1✔
2257
            allow_alphaband_as_mask: true,
1✔
2258
            retry: None,
1✔
2259
        };
1✔
2260

1✔
2261
        let tile_information =
1✔
2262
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2263

1✔
2264
        let RasterTile2D {
1✔
2265
            global_geo_transform: _,
1✔
2266
            grid_array: grid,
1✔
2267
            tile_position: _,
1✔
2268
            band: _,
1✔
2269
            time: _,
1✔
2270
            properties,
1✔
2271
            cache_hint: _,
1✔
2272
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2273
            &up_side_down_params,
1✔
2274
            tile_information,
1✔
2275
            TimeInterval::default(),
1✔
2276
            CacheHint::default(),
1✔
2277
        )
1✔
2278
        .unwrap();
1✔
2279

1✔
2280
        assert!(!grid.is_empty());
1✔
2281

2282
        let grid = grid.into_materialized_masked_grid();
1✔
2283

1✔
2284
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2285
        assert_eq!(
1✔
2286
            grid.inner_grid.data,
1✔
2287
            &[
1✔
2288
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2289
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2290
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2291
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2292
            ]
1✔
2293
        );
1✔
2294

2295
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2296
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2297

2298
        assert_eq!(properties.offset_option(), Some(37.));
1✔
2299
        assert_eq!(properties.scale_option(), Some(3.7));
1✔
2300

2301
        assert!(approx_eq!(f64, properties.offset(), 37.));
1✔
2302
        assert!(approx_eq!(f64, properties.scale(), 3.7));
1✔
2303
    }
1✔
2304

2305
    #[test]
2306
    #[allow(clippy::too_many_lines)]
2307
    fn it_creates_no_data_only_for_missing_files() {
1✔
2308
        hide_gdal_errors();
1✔
2309

1✔
2310
        let ds = GdalDatasetParameters {
1✔
2311
            file_path: "nonexisting_file.tif".into(),
1✔
2312
            rasterband_channel: 1,
1✔
2313
            geo_transform: TestDefault::test_default(),
1✔
2314
            width: 100,
1✔
2315
            height: 100,
1✔
2316
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2317
            no_data_value: None,
1✔
2318
            properties_mapping: None,
1✔
2319
            gdal_open_options: None,
1✔
2320
            gdal_config_options: None,
1✔
2321
            allow_alphaband_as_mask: true,
1✔
2322
            retry: None,
1✔
2323
        };
1✔
2324

1✔
2325
        let tile_info = TileInformation {
1✔
2326
            tile_size_in_pixels: [100, 100].into(),
1✔
2327
            global_tile_position: [0, 0].into(),
1✔
2328
            global_geo_transform: TestDefault::test_default(),
1✔
2329
        };
1✔
2330

1✔
2331
        let tile_time = TimeInterval::default();
1✔
2332

1✔
2333
        // file doesn't exist => no data
1✔
2334
        let result =
1✔
2335
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2336
                .unwrap();
1✔
2337
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2338

2339
        let ds = GdalDatasetParameters {
1✔
2340
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2341
            rasterband_channel: 100, // invalid channel
1✔
2342
            geo_transform: TestDefault::test_default(),
1✔
2343
            width: 100,
1✔
2344
            height: 100,
1✔
2345
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2346
            no_data_value: None,
1✔
2347
            properties_mapping: None,
1✔
2348
            gdal_open_options: None,
1✔
2349
            gdal_config_options: None,
1✔
2350
            allow_alphaband_as_mask: true,
1✔
2351
            retry: None,
1✔
2352
        };
1✔
2353

1✔
2354
        // invalid channel => error
1✔
2355
        let result =
1✔
2356
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2357
        assert!(result.is_err());
1✔
2358

2359
        let server = Server::run();
1✔
2360

1✔
2361
        server.expect(
1✔
2362
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2363
                .times(1)
1✔
2364
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2365
        );
1✔
2366

1✔
2367
        server.expect(
1✔
2368
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2369
                .times(1)
1✔
2370
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2371
        );
1✔
2372

1✔
2373
        let ds = GdalDatasetParameters {
1✔
2374
            file_path: format!("/vsicurl/{}", server.url_str("/non_existing.tif")).into(),
1✔
2375
            rasterband_channel: 1,
1✔
2376
            geo_transform: TestDefault::test_default(),
1✔
2377
            width: 100,
1✔
2378
            height: 100,
1✔
2379
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2380
            no_data_value: None,
1✔
2381
            properties_mapping: None,
1✔
2382
            gdal_open_options: None,
1✔
2383
            gdal_config_options: Some(vec![
1✔
2384
                (
1✔
2385
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2386
                    ".tif".to_owned(),
1✔
2387
                ),
1✔
2388
                (
1✔
2389
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2390
                    "EMPTY_DIR".to_owned(),
1✔
2391
                ),
1✔
2392
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2393
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2394
            ]),
1✔
2395
            allow_alphaband_as_mask: true,
1✔
2396
            retry: None,
1✔
2397
        };
1✔
2398

1✔
2399
        // 404 => no data
1✔
2400
        let result =
1✔
2401
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2402
                .unwrap();
1✔
2403
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2404

2405
        let ds = GdalDatasetParameters {
1✔
2406
            file_path: format!("/vsicurl/{}", server.url_str("/internal_error.tif")).into(),
1✔
2407
            rasterband_channel: 1,
1✔
2408
            geo_transform: TestDefault::test_default(),
1✔
2409
            width: 100,
1✔
2410
            height: 100,
1✔
2411
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2412
            no_data_value: None,
1✔
2413
            properties_mapping: None,
1✔
2414
            gdal_open_options: None,
1✔
2415
            gdal_config_options: Some(vec![
1✔
2416
                (
1✔
2417
                    "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2418
                    ".tif".to_owned(),
1✔
2419
                ),
1✔
2420
                (
1✔
2421
                    "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2422
                    "EMPTY_DIR".to_owned(),
1✔
2423
                ),
1✔
2424
                ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2425
                ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2426
            ]),
1✔
2427
            allow_alphaband_as_mask: true,
1✔
2428
            retry: None,
1✔
2429
        };
1✔
2430

1✔
2431
        // 500 => error
1✔
2432
        let result =
1✔
2433
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2434
        assert!(result.is_err());
1✔
2435
    }
1✔
2436

2437
    #[test]
2438
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2439
        hide_gdal_errors();
1✔
2440

1✔
2441
        let server = Server::run();
1✔
2442

1✔
2443
        server.expect(
1✔
2444
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2445
                .times(2)
1✔
2446
                .respond_with(responders::cycle![
1✔
2447
                    // first generic error
1✔
2448
                    responders::status_code(500),
1✔
2449
                    // then 404 file not found
1✔
2450
                    responders::status_code(404)
1✔
2451
                ]),
1✔
2452
        );
1✔
2453

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

1✔
2456
        let options = Some(vec![
1✔
2457
            (
1✔
2458
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2459
                ".tif".to_owned(),
1✔
2460
            ),
1✔
2461
            (
1✔
2462
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2463
                "EMPTY_DIR".to_owned(),
1✔
2464
            ),
1✔
2465
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2466
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2467
        ]);
1✔
2468

1✔
2469
        let _thread_local_configs = options
1✔
2470
            .as_ref()
1✔
2471
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2472

1✔
2473
        // first fail
1✔
2474
        let result = gdal_open_dataset_ex(
1✔
2475
            file_path.as_path(),
1✔
2476
            DatasetOptions {
1✔
2477
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2478
                ..DatasetOptions::default()
1✔
2479
            },
1✔
2480
        );
1✔
2481

1✔
2482
        // it failed, but not with file not found
1✔
2483
        assert!(result.is_err());
1✔
2484
        if let Err(error) = result {
1✔
2485
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2486
        }
×
2487

2488
        // second fail doesn't even try, so still not "file not found", even though it should be now
2489
        let result = gdal_open_dataset_ex(
1✔
2490
            file_path.as_path(),
1✔
2491
            DatasetOptions {
1✔
2492
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2493
                ..DatasetOptions::default()
1✔
2494
            },
1✔
2495
        );
1✔
2496

1✔
2497
        assert!(result.is_err());
1✔
2498
        if let Err(error) = result {
1✔
2499
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2500
        }
×
2501

2502
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2503

1✔
2504
        // after clearing the cache, it tries again
1✔
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
        // now we get the file not found error
1✔
2514
        assert!(result.is_err());
1✔
2515
        if let Err(error) = result {
1✔
2516
            assert!(error_is_gdal_file_not_found(&error));
1✔
2517
        }
×
2518
    }
1✔
2519

2520
    #[tokio::test]
2521
    async fn it_attaches_cache_hint() {
1✔
2522
        let output_bounds =
1✔
2523
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
2524
        let output_shape: GridShape2D = [256, 256].into();
1✔
2525

1✔
2526
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2527
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
2528
        let params = None;
1✔
2529

1✔
2530
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
2531
            params,
1✔
2532
            tile_info,
1✔
2533
            time_interval,
1✔
2534
            CacheHint::seconds(1234),
1✔
2535
        )
1✔
2536
        .await;
1✔
2537

1✔
2538
        assert!(tile.is_ok());
1✔
2539

1✔
2540
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
2541
            time_interval,
1✔
2542
            tile_info,
1✔
2543
            0,
1✔
2544
            EmptyGrid2D::new(output_shape).into(),
1✔
2545
            CacheHint::seconds(1234),
1✔
2546
        );
1✔
2547

1✔
2548
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
2549
    }
1✔
2550
}
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