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

geo-engine / geoengine / 7276004606

20 Dec 2023 01:21PM UTC coverage: 89.793% (-0.03%) from 89.823%
7276004606

push

github

web-flow
Merge pull request #906 from geo-engine/raster_result_describer

result descriptors for query processors

1080 of 1240 new or added lines in 43 files covered. (87.1%)

14 existing lines in 6 files now uncovered.

115914 of 129090 relevant lines covered (89.79%)

58688.76 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

174
/// A user friendly representation of Gdal's geo transform. In contrast to [`GeoTransform`] this
175
/// geo transform allows arbitrary pixel sizes and can thus also represent rasters where the origin is not located
176
/// in the upper left corner. It should only be used for loading rasters with Gdal and not internally.
177
/// The GDAL pixel space is usually anchored at the "top-left" corner of the data spatial bounds. Therefore the raster data is stored with spatial coordinate y-values decreasing with the rasters rows. This is represented by a negative pixel size.
178
/// However, there are datasets where the data is stored "upside-down". If this is the case, the pixel size is positive.
179
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, FromSql, ToSql)]
2,364✔
180
#[serde(rename_all = "camelCase")]
181
pub struct GdalDatasetGeoTransform {
182
    pub origin_coordinate: Coordinate2D,
183
    pub x_pixel_size: f64,
184
    pub y_pixel_size: f64,
185
}
186

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

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

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

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

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

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

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

227
    fn spatial_partition_to_read_window(
831✔
228
        &self,
831✔
229
        spatial_partition: &SpatialPartition2D,
831✔
230
    ) -> GdalReadWindow {
831✔
231
        // World coordinates and pixel sizes use float values. Since the float imprecision might cause overflowing into the next pixel we use an epsilon to correct values very close the pixel borders. This logic is the same as used in [`GeoTransform::grid_idx_to_pixel_upper_left_coordinate_2d`].
831✔
232
        const EPSILON: f64 = 0.000_001;
831✔
233
        let epsilon: Coordinate2D =
831✔
234
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
831✔
235

236
        /*
237
        The read window is relative to the transform of the gdal dataset. The `SpatialPartition` is oriented at axis of the spatial SRS. This usually causes this situation:
238

239
        The gdal data is stored with negative pixel size. The "ul" coordinate of the `SpatialPartition` is neareest to the origin of the gdal raster data.
240
        ul                      ur
241
        +_______________________+
242
        |_|_ row 1              |
243
        | |_|_  row 2           |
244
        |   |_|_  row ...       |
245
        |     |_|               |
246
        |_______________________|
247
        +                       *
248
        ll                      lr
249

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

252
        The gdal data is stored with a positive pixel size. So the "ll" coordinate is nearest to the reading the raster data needs to starts at this anchor.
253

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

264
        Therefore we need to select the raster read start based on the coordinate next to the raster data origin. From there we then calculate the size of the window to read.
265
        */
266
        let (near_origin_coord, far_origin_coord) = if self.y_pixel_size.is_sign_negative() {
831✔
267
            (
829✔
268
                spatial_partition.upper_left(),
829✔
269
                spatial_partition.lower_right(),
829✔
270
            )
829✔
271
        } else {
272
            (
2✔
273
                spatial_partition.lower_left(),
2✔
274
                spatial_partition.upper_right(),
2✔
275
            )
2✔
276
        };
277

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

429
struct GdalRasterLoader {}
430

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

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

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

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

472
                            Err(e)
6✔
473
                        }
474
                    }
475
                }
809✔
476
            },
828✔
477
        )
822✔
478
        .await
799✔
479
    }
803✔
480

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

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

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

512
                Ok(create_no_data_tile(tile_information, tile_time, cache_hint))
46✔
513
            }
514
        }
515
    }
882✔
516

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

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

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

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

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

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

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

830✔
576
        let dataset = dataset_result.expect("checked");
830✔
577

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

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

589
        Ok(result_tile)
827✔
590
    }
837✔
591

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

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

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

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

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

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

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

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

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

685
        let spatial_resolution = query.spatial_resolution;
113✔
686

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

700
        let tiling_strategy = self
113✔
701
            .tiling_specification
113✔
702
            .strategy(pixel_size_x, pixel_size_y);
113✔
703

704
        let result_descriptor = self.meta_data.result_descriptor().await?;
113✔
705

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

710
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
113✔
711
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
74✔
712
                debug!("query does not intersect spatial data bounds");
2✔
713
                empty = true;
2✔
714
            }
72✔
715
        }
39✔
716

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

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

736
        let source_stream = stream::iter(loading_iter);
113✔
737

113✔
738
        let source_stream = GdalRasterLoader::loading_info_to_tile_stream(
113✔
739
            source_stream,
113✔
740
            query.clone(),
113✔
741
            tiling_strategy,
113✔
742
        );
113✔
743

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

756
    fn result_descriptor(&self) -> &RasterResultDescriptor {
260✔
757
        &self.result_descriptor
260✔
758
    }
260✔
759
}
760

761
pub type GdalSource = SourceOperator<GdalSourceParameters>;
762

763
impl OperatorName for GdalSource {
764
    const TYPE_NAME: &'static str = "GdalSource";
765
}
766

767
#[typetag::serde]
32✔
768
#[async_trait]
769
impl RasterOperator for GdalSource {
770
    async fn _initialize(
51✔
771
        self: Box<Self>,
51✔
772
        path: WorkflowOperatorPath,
51✔
773
        context: &dyn crate::engine::ExecutionContext,
51✔
774
    ) -> Result<Box<dyn InitializedRasterOperator>> {
51✔
775
        let data_id = context.resolve_named_data(&self.params.data).await?;
66✔
776
        let meta_data: GdalMetaData = context.meta_data(&data_id).await?;
83✔
777

778
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
50✔
779
        debug!("GdalSource path: {:?}", path);
50✔
780

781
        let op = InitializedGdalSourceOperator {
50✔
782
            name: CanonicOperatorName::from(&self),
50✔
783
            result_descriptor: meta_data.result_descriptor().await?,
50✔
784
            meta_data,
50✔
785
            tiling_specification: context.tiling_specification(),
50✔
786
        };
50✔
787

50✔
788
        Ok(op.boxed())
50✔
789
    }
102✔
790

791
    span_fn!(GdalSource);
1✔
792
}
793

794
pub struct InitializedGdalSourceOperator {
795
    name: CanonicOperatorName,
796
    pub meta_data: GdalMetaData,
797
    pub result_descriptor: RasterResultDescriptor,
798
    pub tiling_specification: TilingSpecification,
799
}
800

801
impl InitializedRasterOperator for InitializedGdalSourceOperator {
802
    fn result_descriptor(&self) -> &RasterResultDescriptor {
89✔
803
        &self.result_descriptor
89✔
804
    }
89✔
805

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

889
    fn canonic_name(&self) -> CanonicOperatorName {
1✔
890
        self.name.clone()
1✔
891
    }
1✔
892
}
893

894
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
895
/// It fails if the tile is not within the dataset.
896
#[allow(clippy::float_cmp)]
897
fn read_grid_from_raster<T, D>(
829✔
898
    rasterband: &GdalRasterBand,
829✔
899
    read_window: &GdalReadWindow,
829✔
900
    out_shape: D,
829✔
901
    dataset_params: &GdalDatasetParameters,
829✔
902
    flip_y_axis: bool,
829✔
903
) -> Result<GridOrEmpty<D, T>>
829✔
904
where
829✔
905
    T: Pixel + GdalType + Default + FromPrimitive,
829✔
906
    D: Clone + GridSize + PartialEq,
829✔
907
{
829✔
908
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
829✔
909

910
    let buffer = rasterband.read_as::<T>(
829✔
911
        read_window.gdal_window_start(), // pixelspace origin
829✔
912
        read_window.gdal_window_size(),  // pixelspace size
829✔
913
        gdal_out_shape,                  // requested raster size
829✔
914
        None,                            // sampling mode
829✔
915
    )?;
829✔
916
    let data_grid = Grid::new(out_shape.clone(), buffer.data)?;
827✔
917

918
    let data_grid = if flip_y_axis {
827✔
919
        data_grid.reversed_y_axis_grid()
1✔
920
    } else {
921
        data_grid
826✔
922
    };
923

924
    let dataset_mask_flags = rasterband.mask_flags()?;
827✔
925

926
    if dataset_mask_flags.is_all_valid() {
827✔
927
        debug!("all pixels are valid --> skip no-data and mask handling.");
×
928
        return Ok(MaskedGrid::new_with_data(data_grid).into());
×
929
    }
827✔
930

827✔
931
    if dataset_mask_flags.is_nodata() {
827✔
932
        debug!("raster uses a no-data value --> use no-data handling.");
819✔
933
        let no_data_value = dataset_params
819✔
934
            .no_data_value
819✔
935
            .or_else(|| rasterband.no_data_value())
819✔
936
            .and_then(FromPrimitive::from_f64);
819✔
937
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
819✔
938
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
819✔
939
        return Ok(grid_or_empty);
819✔
940
    }
8✔
941

8✔
942
    if dataset_mask_flags.is_alpha() {
8✔
943
        debug!("raster uses alpha band to mask pixels.");
×
944
        if !dataset_params.allow_alphaband_as_mask {
×
945
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
946
        }
×
947
    }
8✔
948

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

951
    let mask_band = rasterband.open_mask_band()?;
8✔
952
    let mask_buffer = mask_band.read_as::<u8>(
8✔
953
        read_window.gdal_window_start(), // pixelspace origin
8✔
954
        read_window.gdal_window_size(),  // pixelspace size
8✔
955
        gdal_out_shape,                  // requested raster size
8✔
956
        None,                            // sampling mode
8✔
957
    )?;
8✔
958
    let mask_grid = Grid::new(out_shape, mask_buffer.data)?.map_elements(|p: u8| p > 0);
360,016✔
959

960
    let mask_grid = if flip_y_axis {
8✔
961
        mask_grid.reversed_y_axis_grid()
×
962
    } else {
963
        mask_grid
8✔
964
    };
965

966
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
8✔
967
    Ok(GridOrEmpty::from(masked_grid))
8✔
968
}
829✔
969

970
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
971
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
972
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
973
fn read_partial_grid_from_raster<T>(
485✔
974
    rasterband: &GdalRasterBand,
485✔
975
    gdal_read_window: &GdalReadWindow,
485✔
976
    out_tile_read_bounds: GridBoundingBox2D,
485✔
977
    out_tile_shape: GridShape2D,
485✔
978
    dataset_params: &GdalDatasetParameters,
485✔
979
    flip_y_axis: bool,
485✔
980
) -> Result<GridOrEmpty2D<T>>
485✔
981
where
485✔
982
    T: Pixel + GdalType + Default + FromPrimitive,
485✔
983
{
485✔
984
    let dataset_raster = read_grid_from_raster(
485✔
985
        rasterband,
485✔
986
        gdal_read_window,
485✔
987
        out_tile_read_bounds,
485✔
988
        dataset_params,
485✔
989
        flip_y_axis,
485✔
990
    )?;
485✔
991

992
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
483✔
993
    tile_raster.grid_blit_from(&dataset_raster);
483✔
994
    Ok(tile_raster)
483✔
995
}
485✔
996

997
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
998
/// It handles conversion to grid coordinates.
999
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
1000
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
1001
fn read_grid_and_handle_edges<T>(
829✔
1002
    tile_info: TileInformation,
829✔
1003
    dataset: &GdalDataset,
829✔
1004
    rasterband: &GdalRasterBand,
829✔
1005
    dataset_params: &GdalDatasetParameters,
829✔
1006
) -> Result<GridOrEmpty2D<T>>
829✔
1007
where
829✔
1008
    T: Pixel + GdalType + Default + FromPrimitive,
829✔
1009
{
829✔
1010
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
829✔
1011
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
829✔
1012

829✔
1013
    if !approx_eq!(
829✔
1014
        GdalDatasetGeoTransform,
829✔
1015
        gdal_dataset_geotransform,
829✔
1016
        dataset_params.geo_transform
829✔
1017
    ) {
829✔
1018
        log::warn!(
1✔
1019
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
×
1020
            dataset_params.geo_transform,
1021
            gdal_dataset_geotransform,
1022
        );
1023
    };
828✔
1024

1025
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
829✔
1026
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
829✔
1027

1028
    let gdal_dataset_bounds =
829✔
1029
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
829✔
1030

829✔
1031
    let output_bounds = tile_info.spatial_partition();
829✔
1032
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
829✔
1033
    let output_shape = tile_info.tile_size_in_pixels();
829✔
1034

1035
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
829✔
1036
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
1037
    };
1038

1039
    let tile_geo_transform = tile_info.tile_geo_transform();
829✔
1040

829✔
1041
    let gdal_read_window =
829✔
1042
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
829✔
1043

829✔
1044
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
829✔
1045
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
829✔
1046

829✔
1047
    if is_y_axis_flipped {
829✔
1048
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
1049
    }
828✔
1050

1051
    let result_grid = if dataset_intersection_area == output_bounds {
829✔
1052
        read_grid_from_raster(
344✔
1053
            rasterband,
344✔
1054
            &gdal_read_window,
344✔
1055
            output_shape,
344✔
1056
            dataset_params,
344✔
1057
            is_y_axis_flipped,
344✔
1058
        )?
344✔
1059
    } else {
1060
        let partial_tile_grid_bounds =
485✔
1061
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
485✔
1062

485✔
1063
        read_partial_grid_from_raster(
485✔
1064
            rasterband,
485✔
1065
            &gdal_read_window,
485✔
1066
            partial_tile_grid_bounds,
485✔
1067
            output_shape,
485✔
1068
            dataset_params,
485✔
1069
            is_y_axis_flipped,
485✔
1070
        )?
485✔
1071
    };
1072

1073
    Ok(result_grid)
827✔
1074
}
829✔
1075

1076
/// 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.
1077
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
830✔
1078
    dataset: &GdalDataset,
830✔
1079
    dataset_params: &GdalDatasetParameters,
830✔
1080
    tile_info: TileInformation,
830✔
1081
    tile_time: TimeInterval,
830✔
1082
    cache_hint: CacheHint,
830✔
1083
) -> Result<RasterTile2D<T>> {
830✔
1084
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
830✔
1085

1086
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
829✔
1087

1088
    let mut properties = RasterProperties::default();
827✔
1089

827✔
1090
    // always read the scale and offset values from the rasterband
827✔
1091
    properties_from_band(&mut properties, &rasterband);
827✔
1092

1093
    // read the properties from the dataset and rasterband metadata
1094
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
827✔
1095
        properties_from_gdal_metadata(&mut properties, dataset, properties_mapping);
4✔
1096
        properties_from_gdal_metadata(&mut properties, &rasterband, properties_mapping);
4✔
1097
    }
823✔
1098

1099
    // TODO: add cache_hint
1100
    Ok(RasterTile2D::new_with_tile_info_and_properties(
827✔
1101
        tile_time,
827✔
1102
        tile_info,
827✔
1103
        0,
827✔
1104
        result_grid,
827✔
1105
        properties,
827✔
1106
        cache_hint,
827✔
1107
    ))
827✔
1108
}
830✔
1109

1110
fn create_no_data_tile<T: Pixel>(
81✔
1111
    tile_info: TileInformation,
81✔
1112
    tile_time: TimeInterval,
81✔
1113
    cache_hint: CacheHint,
81✔
1114
) -> RasterTile2D<T> {
81✔
1115
    // TODO: add cache_hint
81✔
1116
    RasterTile2D::new_with_tile_info_and_properties(
81✔
1117
        tile_time,
81✔
1118
        tile_info,
81✔
1119
        0,
81✔
1120
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
81✔
1121
        RasterProperties::default(),
81✔
1122
        cache_hint,
81✔
1123
    )
81✔
1124
}
81✔
1125

1126
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, FromSql, ToSql)]
2,364✔
1127
pub struct GdalMetadataMapping {
1128
    pub source_key: RasterPropertiesKey,
1129
    pub target_key: RasterPropertiesKey,
1130
    pub target_type: RasterPropertiesEntryType,
1131
}
1132

1133
impl GdalMetadataMapping {
1134
    pub fn identity(
×
1135
        key: RasterPropertiesKey,
×
1136
        target_type: RasterPropertiesEntryType,
×
1137
    ) -> GdalMetadataMapping {
×
1138
        GdalMetadataMapping {
×
1139
            source_key: key.clone(),
×
1140
            target_key: key,
×
1141
            target_type,
×
1142
        }
×
1143
    }
×
1144
}
1145

1146
fn properties_from_gdal_metadata<'a, I, M>(
8✔
1147
    properties: &mut RasterProperties,
8✔
1148
    gdal_dataset: &M,
8✔
1149
    properties_mapping: I,
8✔
1150
) where
8✔
1151
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
8✔
1152
    M: GdalMetadata,
8✔
1153
{
8✔
1154
    let mapping_iter = properties_mapping.into_iter();
8✔
1155

1156
    for m in mapping_iter {
24✔
1157
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1158
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1159
        } else {
1160
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1161
        };
1162

1163
        if let Some(d) = data {
16✔
1164
            let entry = match m.target_type {
8✔
1165
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1166
                    |_| RasterPropertiesEntry::String(d),
×
1167
                    RasterPropertiesEntry::Number,
×
1168
                ),
×
1169
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1170
            };
1171

1172
            debug!(
8✔
1173
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1174
                &m.source_key, &m.target_key, &entry
×
1175
            );
1176

1177
            properties.insert_property(m.target_key.clone(), entry);
8✔
1178
        }
8✔
1179
    }
1180
}
8✔
1181

1182
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
1183
    if let Some(scale) = gdal_dataset.scale() {
827✔
1184
        properties.set_scale(scale);
1✔
1185
    };
826✔
1186
    if let Some(offset) = gdal_dataset.offset() {
827✔
1187
        properties.set_offset(offset);
1✔
1188
    };
826✔
1189

1190
    // ignore if there is no description
1191
    if let Ok(description) = gdal_dataset.description() {
827✔
1192
        properties.set_description(description);
827✔
1193
    }
827✔
1194
}
827✔
1195

1196
#[cfg(test)]
1197
mod tests {
1198
    use super::*;
1199
    use crate::engine::{MockExecutionContext, MockQueryContext};
1200
    use crate::test_data;
1201
    use crate::util::gdal::add_ndvi_dataset;
1202
    use crate::util::Result;
1203
    use geoengine_datatypes::hashmap;
1204
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1205
    use geoengine_datatypes::raster::{
1206
        EmptyGrid2D, GridBounds, GridIdx2D, TilesEqualIgnoringCacheHint,
1207
    };
1208
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1209
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1210
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1211
    use httptest::matchers::request;
1212
    use httptest::{responders, Expectation, Server};
1213

1214
    async fn query_gdal_source(
5✔
1215
        exe_ctx: &MockExecutionContext,
5✔
1216
        query_ctx: &MockQueryContext,
5✔
1217
        name: NamedData,
5✔
1218
        output_shape: GridShape2D,
5✔
1219
        output_bounds: SpatialPartition2D,
5✔
1220
        time_interval: TimeInterval,
5✔
1221
    ) -> Vec<Result<RasterTile2D<u8>>> {
5✔
1222
        let op = GdalSource {
5✔
1223
            params: GdalSourceParameters { data: name.clone() },
5✔
1224
        }
5✔
1225
        .boxed();
5✔
1226

5✔
1227
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1228
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1229
        let spatial_resolution =
5✔
1230
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1231

1232
        let o = op
5✔
1233
            .initialize(WorkflowOperatorPath::initialize_root(), exe_ctx)
5✔
1234
            .await
×
1235
            .unwrap();
5✔
1236

5✔
1237
        o.query_processor()
5✔
1238
            .unwrap()
5✔
1239
            .get_u8()
5✔
1240
            .unwrap()
5✔
1241
            .raster_query(
5✔
1242
                RasterQueryRectangle {
5✔
1243
                    spatial_bounds: output_bounds,
5✔
1244
                    time_interval,
5✔
1245
                    spatial_resolution,
5✔
1246
                    attributes: BandSelection::first(),
5✔
1247
                },
5✔
1248
                query_ctx,
5✔
1249
            )
5✔
1250
            .await
×
1251
            .unwrap()
5✔
1252
            .collect()
5✔
1253
            .await
14✔
1254
    }
5✔
1255

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

1308
    #[test]
1✔
1309
    fn tiling_strategy_origin() {
1✔
1310
        let tile_size_in_pixels = [600, 600];
1✔
1311
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1312
        let dataset_x_pixel_size = 0.1;
1✔
1313
        let dataset_y_pixel_size = -0.1;
1✔
1314
        let dataset_geo_transform = GeoTransform::new(
1✔
1315
            dataset_upper_right_coord,
1✔
1316
            dataset_x_pixel_size,
1✔
1317
            dataset_y_pixel_size,
1✔
1318
        );
1✔
1319

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

1✔
1322
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1323
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1324
            geo_transform: dataset_geo_transform,
1✔
1325
        };
1✔
1326

1✔
1327
        assert_eq!(
1✔
1328
            origin_split_tileing_strategy
1✔
1329
                .geo_transform
1✔
1330
                .upper_left_pixel_idx(&partition),
1✔
1331
            [0, 0].into()
1✔
1332
        );
1✔
1333
        assert_eq!(
1✔
1334
            origin_split_tileing_strategy
1✔
1335
                .geo_transform
1✔
1336
                .lower_right_pixel_idx(&partition),
1✔
1337
            [1800 - 1, 3600 - 1].into()
1✔
1338
        );
1✔
1339

1340
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1341
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1342
        assert_eq!(tile_grid.min_index(), [0, 0].into());
1✔
1343
        assert_eq!(tile_grid.max_index(), [2, 5].into());
1✔
1344
    }
1✔
1345

1346
    #[test]
1✔
1347
    fn tiling_strategy_zero() {
1✔
1348
        let tile_size_in_pixels = [600, 600];
1✔
1349
        let dataset_x_pixel_size = 0.1;
1✔
1350
        let dataset_y_pixel_size = -0.1;
1✔
1351
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1352
            0.0,
1✔
1353
            dataset_x_pixel_size,
1✔
1354
            0.0,
1✔
1355
            dataset_y_pixel_size,
1✔
1356
        );
1✔
1357

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

1✔
1360
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1361
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1362
            geo_transform: central_geo_transform,
1✔
1363
        };
1✔
1364

1✔
1365
        assert_eq!(
1✔
1366
            origin_split_tileing_strategy
1✔
1367
                .geo_transform
1✔
1368
                .upper_left_pixel_idx(&partition),
1✔
1369
            [-900, -1800].into()
1✔
1370
        );
1✔
1371
        assert_eq!(
1✔
1372
            origin_split_tileing_strategy
1✔
1373
                .geo_transform
1✔
1374
                .lower_right_pixel_idx(&partition),
1✔
1375
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1376
        );
1✔
1377

1378
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1379
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1380
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
1✔
1381
        assert_eq!(tile_grid.max_index(), [1, 2].into());
1✔
1382
    }
1✔
1383

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

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

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

1✔
1403
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1404
            .tile_idx_iterator(partition)
1✔
1405
            .collect();
1✔
1406
        assert_eq!(vres.len(), 4 * 6);
1✔
1407
        assert_eq!(vres[0], [-2, -3].into());
1✔
1408
        assert_eq!(vres[1], [-2, -2].into());
1✔
1409
        assert_eq!(vres[2], [-2, -1].into());
1✔
1410
        assert_eq!(vres[23], [1, 2].into());
1✔
1411
    }
1✔
1412

1413
    #[test]
1✔
1414
    fn tile_information_iterator() {
1✔
1415
        let tile_size_in_pixels = [600, 600];
1✔
1416
        let dataset_x_pixel_size = 0.1;
1✔
1417
        let dataset_y_pixel_size = -0.1;
1✔
1418

1✔
1419
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1420
            0.0,
1✔
1421
            dataset_x_pixel_size,
1✔
1422
            0.0,
1✔
1423
            dataset_y_pixel_size,
1✔
1424
        );
1✔
1425

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

1✔
1428
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1429
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1430
            geo_transform: central_geo_transform,
1✔
1431
        };
1✔
1432

1✔
1433
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1434
            .tile_information_iterator(partition)
1✔
1435
            .collect();
1✔
1436
        assert_eq!(vres.len(), 4 * 6);
1✔
1437
        assert_eq!(
1✔
1438
            vres[0],
1✔
1439
            TileInformation::new(
1✔
1440
                [-2, -3].into(),
1✔
1441
                tile_size_in_pixels.into(),
1✔
1442
                central_geo_transform,
1✔
1443
            )
1✔
1444
        );
1✔
1445
        assert_eq!(
1✔
1446
            vres[1],
1✔
1447
            TileInformation::new(
1✔
1448
                [-2, -2].into(),
1✔
1449
                tile_size_in_pixels.into(),
1✔
1450
                central_geo_transform,
1✔
1451
            )
1✔
1452
        );
1✔
1453
        assert_eq!(
1✔
1454
            vres[12],
1✔
1455
            TileInformation::new(
1✔
1456
                [0, -3].into(),
1✔
1457
                tile_size_in_pixels.into(),
1✔
1458
                central_geo_transform,
1✔
1459
            )
1✔
1460
        );
1✔
1461
        assert_eq!(
1✔
1462
            vres[23],
1✔
1463
            TileInformation::new(
1✔
1464
                [1, 2].into(),
1✔
1465
                tile_size_in_pixels.into(),
1✔
1466
                central_geo_transform,
1✔
1467
            )
1✔
1468
        );
1✔
1469
    }
1✔
1470

1471
    #[test]
1✔
1472
    fn replace_time_placeholder() {
1✔
1473
        let params = GdalDatasetParameters {
1✔
1474
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1475
            rasterband_channel: 0,
1✔
1476
            geo_transform: TestDefault::test_default(),
1✔
1477
            width: 360,
1✔
1478
            height: 180,
1✔
1479
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1480
            no_data_value: Some(0.),
1✔
1481
            properties_mapping: None,
1✔
1482
            gdal_open_options: None,
1✔
1483
            gdal_config_options: None,
1✔
1484
            allow_alphaband_as_mask: true,
1✔
1485
            retry: None,
1✔
1486
        };
1✔
1487
        let replaced = params
1✔
1488
            .replace_time_placeholders(
1✔
1489
                &hashmap! {
1✔
1490
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
1✔
1491
                        format: DateTimeParseFormat::custom("%f".to_string()),
1✔
1492
                        reference: TimeReference::Start,
1✔
1493
                    },
1✔
1494
                },
1✔
1495
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
1✔
1496
            )
1✔
1497
            .unwrap();
1✔
1498
        assert_eq!(
1✔
1499
            replaced.file_path.to_string_lossy(),
1✔
1500
            "/foo/bar_022000000.tiff".to_string()
1✔
1501
        );
1✔
1502
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1503
        assert_eq!(params.geo_transform, replaced.geo_transform);
1✔
1504
        assert_eq!(params.width, replaced.width);
1✔
1505
        assert_eq!(params.height, replaced.height);
1✔
1506
        assert_eq!(
1✔
1507
            params.file_not_found_handling,
1✔
1508
            replaced.file_not_found_handling
1✔
1509
        );
1✔
1510
    }
1✔
1511

1512
    #[test]
1✔
1513
    fn test_load_tile_data() {
1✔
1514
        let output_shape: GridShape2D = [8, 8].into();
1✔
1515
        let output_bounds =
1✔
1516
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1517

1✔
1518
        let RasterTile2D {
1✔
1519
            global_geo_transform: _,
1✔
1520
            grid_array: grid,
1✔
1521
            tile_position: _,
1✔
1522
            band: _,
1✔
1523
            time: _,
1✔
1524
            properties,
1✔
1525
            cache_hint: _,
1✔
1526
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1527

1✔
1528
        assert!(!grid.is_empty());
1✔
1529

1530
        let grid = grid.into_materialized_masked_grid();
1✔
1531

1✔
1532
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
1533
        assert_eq!(
1✔
1534
            grid.inner_grid.data,
1✔
1535
            &[
1✔
1536
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
1537
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
1538
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
1539
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
1540
            ]
1✔
1541
        );
1✔
1542

1543
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1544
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1545

1546
        assert!((properties.scale_option()).is_none());
1✔
1547
        assert!(properties.offset_option().is_none());
1✔
1548
        assert_eq!(
1✔
1549
            properties.get_property(&RasterPropertiesKey {
1✔
1550
                key: "AREA_OR_POINT".to_string(),
1✔
1551
                domain: None,
1✔
1552
            }),
1✔
1553
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1554
        );
1✔
1555
        assert_eq!(
1✔
1556
            properties.get_property(&RasterPropertiesKey {
1✔
1557
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1558
                key: "COMPRESSION".to_string(),
1✔
1559
            }),
1✔
1560
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1561
        );
1✔
1562
    }
1✔
1563

1564
    #[test]
1✔
1565
    fn test_load_tile_data_overlaps_dataset_bounds() {
1✔
1566
        let output_shape: GridShape2D = [8, 8].into();
1✔
1567
        // shift world bbox one pixel up and to the left
1✔
1568
        let (x_size, y_size) = (45., 22.5);
1✔
1569
        let output_bounds = SpatialPartition2D::new_unchecked(
1✔
1570
            (-180. - x_size, 90. + y_size).into(),
1✔
1571
            (180. - x_size, -90. + y_size).into(),
1✔
1572
        );
1✔
1573

1✔
1574
        let RasterTile2D {
1✔
1575
            global_geo_transform: _,
1✔
1576
            grid_array: grid,
1✔
1577
            tile_position: _,
1✔
1578
            band: _,
1✔
1579
            time: _,
1✔
1580
            properties: _,
1✔
1581
            cache_hint: _,
1✔
1582
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1✔
1583

1✔
1584
        assert!(!grid.is_empty());
1✔
1585

1586
        let x = grid.into_materialized_masked_grid();
1✔
1587

1✔
1588
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1589
        assert_eq!(
1✔
1590
            x.inner_grid.data,
1✔
1591
            &[
1✔
1592
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1✔
1593
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1✔
1594
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1✔
1595
                255, 255, 255, 255, 255, 255
1✔
1596
            ]
1✔
1597
        );
1✔
1598
    }
1✔
1599

1600
    #[test]
1✔
1601
    fn test_load_tile_data_is_inside_single_pixel() {
1✔
1602
        let output_shape: GridShape2D = [8, 8].into();
1✔
1603
        // shift world bbox one pixel up and to the left
1✔
1604
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1605
        let output_bounds = SpatialPartition2D::new(
1✔
1606
            (-116.22222, 66.66666).into(),
1✔
1607
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1608
        )
1✔
1609
        .unwrap();
1✔
1610

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

1✔
1621
        assert!(!grid.is_empty());
1✔
1622

1623
        let x = grid.into_materialized_masked_grid();
1✔
1624

1✔
1625
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1626
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1627
    }
1✔
1628

1629
    #[tokio::test]
1✔
1630
    async fn test_query_single_time_slice() {
1✔
1631
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1632
        let query_ctx = MockQueryContext::test_default();
1✔
1633
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1634

1✔
1635
        let output_shape: GridShape2D = [256, 256].into();
1✔
1636
        let output_bounds =
1✔
1637
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1638
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001); // 2014-01-01
1✔
1639

1640
        let c = query_gdal_source(
1✔
1641
            &exe_ctx,
1✔
1642
            &query_ctx,
1✔
1643
            id,
1✔
1644
            output_shape,
1✔
1645
            output_bounds,
1✔
1646
            time_interval,
1✔
1647
        )
1✔
1648
        .await;
5✔
1649
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1650

1✔
1651
        assert_eq!(c.len(), 4);
1✔
1652

1653
        assert_eq!(
1✔
1654
            c[0].time,
1✔
1655
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1656
        );
1✔
1657

1658
        assert_eq!(
1✔
1659
            c[0].tile_information().global_tile_position(),
1✔
1660
            [-1, -1].into()
1✔
1661
        );
1✔
1662

1663
        assert_eq!(
1✔
1664
            c[1].tile_information().global_tile_position(),
1✔
1665
            [-1, 0].into()
1✔
1666
        );
1✔
1667

1668
        assert_eq!(
1✔
1669
            c[2].tile_information().global_tile_position(),
1✔
1670
            [0, -1].into()
1✔
1671
        );
1✔
1672

1673
        assert_eq!(
1✔
1674
            c[3].tile_information().global_tile_position(),
1✔
1675
            [0, 0].into()
1✔
1676
        );
1✔
1677
    }
1678

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

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

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

1✔
1701
        assert_eq!(c.len(), 8);
1✔
1702

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

1708
        assert_eq!(
1✔
1709
            c[5].time,
1✔
1710
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1711
        );
1✔
1712
    }
1713

1714
    #[tokio::test]
1✔
1715
    async fn test_query_before_data() {
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_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1724

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;
×
1734
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1735

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

1738
        assert_eq!(
1✔
1739
            c[0].time,
1✔
1740
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1741
        );
1✔
1742
    }
1743

1744
    #[tokio::test]
1✔
1745
    async fn test_query_after_data() {
1✔
1746
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1747
        let query_ctx = MockQueryContext::test_default();
1✔
1748
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1749

1✔
1750
        let output_shape: GridShape2D = [256, 256].into();
1✔
1751
        let output_bounds =
1✔
1752
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1753
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1754

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

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

1768
        assert_eq!(
1✔
1769
            c[0].time,
1✔
1770
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1771
        );
1✔
1772
    }
1773

1774
    #[tokio::test]
1✔
1775
    async fn test_nodata() {
1✔
1776
        hide_gdal_errors();
1✔
1777

1✔
1778
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1779
        let query_ctx = MockQueryContext::test_default();
1✔
1780
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1781

1✔
1782
        let output_shape: GridShape2D = [256, 256].into();
1✔
1783
        let output_bounds =
1✔
1784
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
1785
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1786

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

1✔
1798
        assert_eq!(c.len(), 4);
1✔
1799

1800
        let tile_1 = &c[0];
1✔
1801

1✔
1802
        assert_eq!(
1✔
1803
            tile_1.time,
1✔
1804
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1805
        );
1✔
1806

1807
        assert!(tile_1.is_empty());
1✔
1808
    }
1809

1810
    #[tokio::test]
1✔
1811
    async fn timestep_without_params() {
1✔
1812
        let output_bounds =
1✔
1813
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1814
        let output_shape: GridShape2D = [256, 256].into();
1✔
1815

1✔
1816
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1817
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1818
        let params = None;
1✔
1819

1820
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
1821
            params,
1✔
1822
            tile_info,
1✔
1823
            time_interval,
1✔
1824
            CacheHint::default(),
1✔
1825
        )
1✔
1826
        .await;
×
1827

1828
        assert!(tile.is_ok());
1✔
1829

1830
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1831
            time_interval,
1✔
1832
            tile_info,
1✔
1833
            0,
1✔
1834
            EmptyGrid2D::new(output_shape).into(),
1✔
1835
            CacheHint::default(),
1✔
1836
        );
1✔
1837

1✔
1838
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
1839
    }
1840

1841
    #[test]
1✔
1842
    fn it_reverts_config_options() {
1✔
1843
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1844

1✔
1845
        {
1✔
1846
            let _config =
1✔
1847
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1848

1✔
1849
            assert_eq!(
1✔
1850
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1851
                "bar".to_owned()
1✔
1852
            );
1✔
1853
        }
1854

1855
        assert_eq!(
1✔
1856
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1857
            String::new()
1✔
1858
        );
1✔
1859
    }
1✔
1860

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

1✔
1906
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1907

1✔
1908
        assert_eq!(
1✔
1909
            dataset_parameters_json,
1✔
1910
            serde_json::json!({
1✔
1911
                "filePath": "path-to-data.tiff",
1✔
1912
                "rasterbandChannel": 1,
1✔
1913
                "geoTransform": {
1✔
1914
                    "originCoordinate": {
1✔
1915
                        "x": -180.,
1✔
1916
                        "y": 90.
1✔
1917
                    },
1✔
1918
                    "xPixelSize": 0.1,
1✔
1919
                    "yPixelSize": -0.1
1✔
1920
                },
1✔
1921
                "width": 3600,
1✔
1922
                "height": 1800,
1✔
1923
                "fileNotFoundHandling": "NoData",
1✔
1924
                "noDataValue": "nan",
1✔
1925
                "propertiesMapping": [{
1✔
1926
                        "source_key": {
1✔
1927
                            "domain": null,
1✔
1928
                            "key": "AREA_OR_POINT"
1✔
1929
                        },
1✔
1930
                        "target_key": {
1✔
1931
                            "domain": null,
1✔
1932
                            "key": "AREA_OR_POINT"
1✔
1933
                        },
1✔
1934
                        "target_type": "String"
1✔
1935
                    },
1✔
1936
                    {
1✔
1937
                        "source_key": {
1✔
1938
                            "domain": "IMAGE_STRUCTURE",
1✔
1939
                            "key": "COMPRESSION"
1✔
1940
                        },
1✔
1941
                        "target_key": {
1✔
1942
                            "domain": "IMAGE_STRUCTURE_INFO",
1✔
1943
                            "key": "COMPRESSION"
1✔
1944
                        },
1✔
1945
                        "target_type": "String"
1✔
1946
                    }
1✔
1947
                ],
1✔
1948
                "gdalOpenOptions": null,
1✔
1949
                "gdalConfigOptions": null,
1✔
1950
                "allowAlphabandAsMask": true,
1✔
1951
                "retry": null,
1✔
1952
            })
1✔
1953
        );
1✔
1954

1955
        let deserialized_parameters =
1✔
1956
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1957

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

1✔
1960
        assert_eq!(
1✔
1961
            deserialized_parameters.file_path,
1✔
1962
            dataset_parameters.file_path,
1✔
1963
        );
1✔
1964
        assert_eq!(
1✔
1965
            deserialized_parameters.rasterband_channel,
1✔
1966
            dataset_parameters.rasterband_channel,
1✔
1967
        );
1✔
1968
        assert_eq!(
1✔
1969
            deserialized_parameters.geo_transform,
1✔
1970
            dataset_parameters.geo_transform,
1✔
1971
        );
1✔
1972
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
1973
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
1974
        assert_eq!(
1✔
1975
            deserialized_parameters.file_not_found_handling,
1✔
1976
            dataset_parameters.file_not_found_handling,
1✔
1977
        );
1✔
1978
        assert!(
1✔
1979
            deserialized_parameters.no_data_value.unwrap().is_nan()
1✔
1980
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
1981
        );
1982
        assert_eq!(
1✔
1983
            deserialized_parameters.properties_mapping,
1✔
1984
            dataset_parameters.properties_mapping,
1✔
1985
        );
1✔
1986
        assert_eq!(
1✔
1987
            deserialized_parameters.gdal_open_options,
1✔
1988
            dataset_parameters.gdal_open_options,
1✔
1989
        );
1✔
1990
        assert_eq!(
1✔
1991
            deserialized_parameters.gdal_config_options,
1✔
1992
            dataset_parameters.gdal_config_options,
1✔
1993
        );
1✔
1994
    }
1✔
1995

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

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

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

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

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

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

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

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

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

1✔
2036
        let sb = gt.spatial_partition(10, 10);
1✔
2037

1✔
2038
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2039
            .unwrap();
1✔
2040

1✔
2041
        assert_eq!(sb, exp);
1✔
2042
    }
1✔
2043

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

1✔
2052
        let sb = gt.spatial_partition(10, 10);
1✔
2053

1✔
2054
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
2055
            .unwrap();
1✔
2056

1✔
2057
        assert_eq!(sb, exp);
1✔
2058
    }
1✔
2059

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

1✔
2068
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
2069
            .unwrap();
1✔
2070

1✔
2071
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2072

1✔
2073
        let exp = GdalReadWindow {
1✔
2074
            read_size_x: 4,
1✔
2075
            read_size_y: 6,
1✔
2076
            read_start_x: 6,
1✔
2077
            read_start_y: 4,
1✔
2078
        };
1✔
2079

1✔
2080
        assert_eq!(rw, exp);
1✔
2081
    }
1✔
2082

2083
    #[test]
1✔
2084
    fn gdal_read_window_data_origin_lower_left() {
1✔
2085
        let gt = GdalDatasetGeoTransform {
1✔
2086
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
2087
            x_pixel_size: 1.,
1✔
2088
            y_pixel_size: 1.,
1✔
2089
        };
1✔
2090

1✔
2091
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
2092
            .unwrap();
1✔
2093

1✔
2094
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
2095

1✔
2096
        let exp = GdalReadWindow {
1✔
2097
            read_size_x: 10,
1✔
2098
            read_size_y: 10,
1✔
2099
            read_start_x: 0,
1✔
2100
            read_start_y: 0,
1✔
2101
        };
1✔
2102

1✔
2103
        assert_eq!(rw, exp);
1✔
2104
    }
1✔
2105

2106
    #[test]
1✔
2107
    fn read_up_side_down_raster() {
1✔
2108
        let output_shape: GridShape2D = [8, 8].into();
1✔
2109
        let output_bounds =
1✔
2110
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2111

1✔
2112
        let up_side_down_params = GdalDatasetParameters {
1✔
2113
            file_path: test_data!(
1✔
2114
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1✔
2115
            )
1✔
2116
            .into(),
1✔
2117
            rasterband_channel: 1,
1✔
2118
            geo_transform: GdalDatasetGeoTransform {
1✔
2119
                origin_coordinate: (-180., -90.).into(),
1✔
2120
                x_pixel_size: 0.1,
1✔
2121
                y_pixel_size: 0.1,
1✔
2122
            },
1✔
2123
            width: 3600,
1✔
2124
            height: 1800,
1✔
2125
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2126
            no_data_value: Some(0.),
1✔
2127
            properties_mapping: Some(vec![
1✔
2128
                GdalMetadataMapping {
1✔
2129
                    source_key: RasterPropertiesKey {
1✔
2130
                        domain: None,
1✔
2131
                        key: "AREA_OR_POINT".to_string(),
1✔
2132
                    },
1✔
2133
                    target_type: RasterPropertiesEntryType::String,
1✔
2134
                    target_key: RasterPropertiesKey {
1✔
2135
                        domain: None,
1✔
2136
                        key: "AREA_OR_POINT".to_string(),
1✔
2137
                    },
1✔
2138
                },
1✔
2139
                GdalMetadataMapping {
1✔
2140
                    source_key: RasterPropertiesKey {
1✔
2141
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1✔
2142
                        key: "COMPRESSION".to_string(),
1✔
2143
                    },
1✔
2144
                    target_type: RasterPropertiesEntryType::String,
1✔
2145
                    target_key: RasterPropertiesKey {
1✔
2146
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
2147
                        key: "COMPRESSION".to_string(),
1✔
2148
                    },
1✔
2149
                },
1✔
2150
            ]),
1✔
2151
            gdal_open_options: None,
1✔
2152
            gdal_config_options: None,
1✔
2153
            allow_alphaband_as_mask: true,
1✔
2154
            retry: None,
1✔
2155
        };
1✔
2156

1✔
2157
        let tile_information =
1✔
2158
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2159

1✔
2160
        let RasterTile2D {
1✔
2161
            global_geo_transform: _,
1✔
2162
            grid_array: grid,
1✔
2163
            tile_position: _,
1✔
2164
            band: _,
1✔
2165
            time: _,
1✔
2166
            properties,
1✔
2167
            cache_hint: _,
1✔
2168
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2169
            &up_side_down_params,
1✔
2170
            tile_information,
1✔
2171
            TimeInterval::default(),
1✔
2172
            CacheHint::default(),
1✔
2173
        )
1✔
2174
        .unwrap();
1✔
2175

1✔
2176
        assert!(!grid.is_empty());
1✔
2177

2178
        let grid = grid.into_materialized_masked_grid();
1✔
2179

1✔
2180
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2181
        assert_eq!(
1✔
2182
            grid.inner_grid.data,
1✔
2183
            &[
1✔
2184
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2185
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2186
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2187
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2188
            ]
1✔
2189
        );
1✔
2190

2191
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2192
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2193

2194
        assert!(properties.offset_option().is_none());
1✔
2195
        assert!(properties.scale_option().is_none());
1✔
2196
    }
1✔
2197

2198
    #[test]
1✔
2199
    fn read_raster_and_offset_scale() {
1✔
2200
        let output_shape: GridShape2D = [8, 8].into();
1✔
2201
        let output_bounds =
1✔
2202
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1✔
2203

1✔
2204
        let up_side_down_params = GdalDatasetParameters {
1✔
2205
            file_path: test_data!(
1✔
2206
                "raster/modis_ndvi/with_offset_scale/MOD13A2_M_NDVI_2014-01-01.TIFF"
1✔
2207
            )
1✔
2208
            .into(),
1✔
2209
            rasterband_channel: 1,
1✔
2210
            geo_transform: GdalDatasetGeoTransform {
1✔
2211
                origin_coordinate: (-180., -90.).into(),
1✔
2212
                x_pixel_size: 0.1,
1✔
2213
                y_pixel_size: 0.1,
1✔
2214
            },
1✔
2215
            width: 3600,
1✔
2216
            height: 1800,
1✔
2217
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2218
            no_data_value: Some(0.),
1✔
2219
            properties_mapping: None,
1✔
2220
            gdal_open_options: None,
1✔
2221
            gdal_config_options: None,
1✔
2222
            allow_alphaband_as_mask: true,
1✔
2223
            retry: None,
1✔
2224
        };
1✔
2225

1✔
2226
        let tile_information =
1✔
2227
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2228

1✔
2229
        let RasterTile2D {
1✔
2230
            global_geo_transform: _,
1✔
2231
            grid_array: grid,
1✔
2232
            tile_position: _,
1✔
2233
            band: _,
1✔
2234
            time: _,
1✔
2235
            properties,
1✔
2236
            cache_hint: _,
1✔
2237
        } = GdalRasterLoader::load_tile_data::<u8>(
1✔
2238
            &up_side_down_params,
1✔
2239
            tile_information,
1✔
2240
            TimeInterval::default(),
1✔
2241
            CacheHint::default(),
1✔
2242
        )
1✔
2243
        .unwrap();
1✔
2244

1✔
2245
        assert!(!grid.is_empty());
1✔
2246

2247
        let grid = grid.into_materialized_masked_grid();
1✔
2248

1✔
2249
        assert_eq!(grid.inner_grid.data.len(), 64);
1✔
2250
        assert_eq!(
1✔
2251
            grid.inner_grid.data,
1✔
2252
            &[
1✔
2253
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1✔
2254
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1✔
2255
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1✔
2256
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1✔
2257
            ]
1✔
2258
        );
1✔
2259

2260
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2261
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2262

2263
        assert_eq!(properties.offset_option(), Some(37.));
1✔
2264
        assert_eq!(properties.scale_option(), Some(3.7));
1✔
2265

2266
        assert!(approx_eq!(f64, properties.offset(), 37.));
1✔
2267
        assert!(approx_eq!(f64, properties.scale(), 3.7));
1✔
2268
    }
1✔
2269

2270
    #[test]
1✔
2271
    #[allow(clippy::too_many_lines)]
2272
    fn it_creates_no_data_only_for_missing_files() {
1✔
2273
        hide_gdal_errors();
1✔
2274

1✔
2275
        let ds = GdalDatasetParameters {
1✔
2276
            file_path: "nonexisting_file.tif".into(),
1✔
2277
            rasterband_channel: 1,
1✔
2278
            geo_transform: TestDefault::test_default(),
1✔
2279
            width: 100,
1✔
2280
            height: 100,
1✔
2281
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2282
            no_data_value: None,
1✔
2283
            properties_mapping: None,
1✔
2284
            gdal_open_options: None,
1✔
2285
            gdal_config_options: None,
1✔
2286
            allow_alphaband_as_mask: true,
1✔
2287
            retry: None,
1✔
2288
        };
1✔
2289

1✔
2290
        let tile_info = TileInformation {
1✔
2291
            tile_size_in_pixels: [100, 100].into(),
1✔
2292
            global_tile_position: [0, 0].into(),
1✔
2293
            global_geo_transform: TestDefault::test_default(),
1✔
2294
        };
1✔
2295

1✔
2296
        let tile_time = TimeInterval::default();
1✔
2297

1✔
2298
        // file doesn't exist => no data
1✔
2299
        let result =
1✔
2300
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2301
                .unwrap();
1✔
2302
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2303

2304
        let ds = GdalDatasetParameters {
1✔
2305
            file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
1✔
2306
            rasterband_channel: 100, // invalid channel
1✔
2307
            geo_transform: TestDefault::test_default(),
1✔
2308
            width: 100,
1✔
2309
            height: 100,
1✔
2310
            file_not_found_handling: FileNotFoundHandling::NoData,
1✔
2311
            no_data_value: None,
1✔
2312
            properties_mapping: None,
1✔
2313
            gdal_open_options: None,
1✔
2314
            gdal_config_options: None,
1✔
2315
            allow_alphaband_as_mask: true,
1✔
2316
            retry: None,
1✔
2317
        };
1✔
2318

1✔
2319
        // invalid channel => error
1✔
2320
        let result =
1✔
2321
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2322
        assert!(result.is_err());
1✔
2323

2324
        let server = Server::run();
1✔
2325

1✔
2326
        server.expect(
1✔
2327
            Expectation::matching(request::method_path("HEAD", "/non_existing.tif"))
1✔
2328
                .times(1)
1✔
2329
                .respond_with(responders::cycle![responders::status_code(404),]),
1✔
2330
        );
1✔
2331

1✔
2332
        server.expect(
1✔
2333
            Expectation::matching(request::method_path("HEAD", "/internal_error.tif"))
1✔
2334
                .times(1)
1✔
2335
                .respond_with(responders::cycle![responders::status_code(500),]),
1✔
2336
        );
1✔
2337

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

1✔
2364
        // 404 => no data
1✔
2365
        let result =
1✔
2366
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default())
1✔
2367
                .unwrap();
1✔
2368
        assert!(matches!(result.grid_array, GridOrEmpty::Empty(_)));
1✔
2369

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

1✔
2396
        // 500 => error
1✔
2397
        let result =
1✔
2398
            GdalRasterLoader::load_tile_data::<u8>(&ds, tile_info, tile_time, CacheHint::default());
1✔
2399
        assert!(result.is_err());
1✔
2400
    }
1✔
2401

2402
    #[test]
1✔
2403
    fn it_retries_only_after_clearing_vsi_cache() {
1✔
2404
        hide_gdal_errors();
1✔
2405

1✔
2406
        let server = Server::run();
1✔
2407

1✔
2408
        server.expect(
1✔
2409
            Expectation::matching(request::method_path("HEAD", "/foo.tif"))
1✔
2410
                .times(2)
1✔
2411
                .respond_with(responders::cycle![
1✔
2412
                    // first generic error
1✔
2413
                    responders::status_code(500),
1✔
2414
                    // then 404 file not found
1✔
2415
                    responders::status_code(404)
1✔
2416
                ]),
1✔
2417
        );
1✔
2418

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

1✔
2421
        let options = Some(vec![
1✔
2422
            (
1✔
2423
                "CPL_VSIL_CURL_ALLOWED_EXTENSIONS".to_owned(),
1✔
2424
                ".tif".to_owned(),
1✔
2425
            ),
1✔
2426
            (
1✔
2427
                "GDAL_DISABLE_READDIR_ON_OPEN".to_owned(),
1✔
2428
                "EMPTY_DIR".to_owned(),
1✔
2429
            ),
1✔
2430
            ("GDAL_HTTP_NETRC".to_owned(), "NO".to_owned()),
1✔
2431
            ("GDAL_HTTP_MAX_RETRY".to_owned(), "0".to_string()),
1✔
2432
        ]);
1✔
2433

1✔
2434
        let _thread_local_configs = options
1✔
2435
            .as_ref()
1✔
2436
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
1✔
2437

1✔
2438
        // first fail
1✔
2439
        let result = gdal_open_dataset_ex(
1✔
2440
            file_path.as_path(),
1✔
2441
            DatasetOptions {
1✔
2442
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2443
                ..DatasetOptions::default()
1✔
2444
            },
1✔
2445
        );
1✔
2446

1✔
2447
        // it failed, but not with file not found
1✔
2448
        assert!(result.is_err());
1✔
2449
        if let Err(error) = result {
1✔
2450
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2451
        }
×
2452

2453
        // second fail doesn't even try, so still not "file not found", even though it should be now
2454
        let result = gdal_open_dataset_ex(
1✔
2455
            file_path.as_path(),
1✔
2456
            DatasetOptions {
1✔
2457
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2458
                ..DatasetOptions::default()
1✔
2459
            },
1✔
2460
        );
1✔
2461

1✔
2462
        assert!(result.is_err());
1✔
2463
        if let Err(error) = result {
1✔
2464
            assert!(!error_is_gdal_file_not_found(&error));
1✔
2465
        }
×
2466

2467
        clear_gdal_vsi_cache_for_path(file_path.as_path());
1✔
2468

1✔
2469
        // after clearing the cache, it tries again
1✔
2470
        let result = gdal_open_dataset_ex(
1✔
2471
            file_path.as_path(),
1✔
2472
            DatasetOptions {
1✔
2473
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
1✔
2474
                ..DatasetOptions::default()
1✔
2475
            },
1✔
2476
        );
1✔
2477

1✔
2478
        // now we get the file not found error
1✔
2479
        assert!(result.is_err());
1✔
2480
        if let Err(error) = result {
1✔
2481
            assert!(error_is_gdal_file_not_found(&error));
1✔
2482
        }
×
2483
    }
1✔
2484

2485
    #[tokio::test]
1✔
2486
    async fn it_attaches_cache_hint() {
1✔
2487
        let output_bounds =
1✔
2488
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
2489
        let output_shape: GridShape2D = [256, 256].into();
1✔
2490

1✔
2491
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2492
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
2493
        let params = None;
1✔
2494

2495
        let tile = GdalRasterLoader::load_tile_async::<f64>(
1✔
2496
            params,
1✔
2497
            tile_info,
1✔
2498
            time_interval,
1✔
2499
            CacheHint::seconds(1234),
1✔
2500
        )
1✔
2501
        .await;
×
2502

2503
        assert!(tile.is_ok());
1✔
2504

2505
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
2506
            time_interval,
1✔
2507
            tile_info,
1✔
2508
            0,
1✔
2509
            EmptyGrid2D::new(output_shape).into(),
1✔
2510
            CacheHint::seconds(1234),
1✔
2511
        );
1✔
2512

1✔
2513
        assert!(tile.unwrap().tiles_equal_ignoring_cache_hint(&expected));
1✔
2514
    }
2515
}
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