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

geo-engine / geoengine / 3679235394

pending completion
3679235394

push

github

GitHub
Merge #692

20 of 20 new or added lines in 2 files covered. (100.0%)

42001 of 50033 relevant lines covered (83.95%)

2.01 hits per line

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

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

55
mod error;
56
mod loading_info;
57

58
/// Parameters for the GDAL Source Operator
59
///
60
/// # Examples
61
///
62
/// ```rust
63
/// use serde_json::{Result, Value};
64
/// use geoengine_operators::source::{GdalSource, GdalSourceParameters};
65
/// use geoengine_datatypes::dataset::{DatasetId, DataId};
66
/// use geoengine_datatypes::util::Identifier;
67
/// use std::str::FromStr;
68
///
69
/// let json_string = r#"
70
///     {
71
///         "type": "GdalSource",
72
///         "params": {
73
///             "data": {
74
///                 "type": "internal",
75
///                 "datasetId": "a626c880-1c41-489b-9e19-9596d129859c"
76
///             }
77
///         }
78
///     }"#;
79
///
80
/// let operator: GdalSource = serde_json::from_str(json_string).unwrap();
81
///
82
/// assert_eq!(operator, GdalSource {
83
///     params: GdalSourceParameters {
84
///         data: DatasetId::from_str("a626c880-1c41-489b-9e19-9596d129859c").unwrap().into()
85
///     },
86
/// });
87
/// ```
88
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
89
pub struct GdalSourceParameters {
90
    pub data: DataId,
91
}
92

93
impl OperatorData for GdalSourceParameters {
94
    fn data_ids_collect(&self, data_ids: &mut Vec<DataId>) {
1✔
95
        data_ids.push(self.data.clone());
1✔
96
    }
97
}
98

99
type GdalMetaData =
100
    Box<dyn MetaData<GdalLoadingInfo, RasterResultDescriptor, RasterQueryRectangle>>;
101

102
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103
#[serde(rename_all = "camelCase")]
104
pub struct GdalSourceTimePlaceholder {
105
    pub format: DateTimeParseFormat,
106
    pub reference: TimeReference,
107
}
108

109
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
110
#[serde(rename_all = "camelCase")]
111
pub enum TimeReference {
112
    Start,
113
    End,
114
}
115

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

140
#[derive(Debug, PartialEq, Eq)]
141
struct GdalReadWindow {
142
    read_start_x: isize, // pixelspace origin
143
    read_start_y: isize,
144
    read_size_x: usize, // pixelspace size
145
    read_size_y: usize,
146
}
147

148
impl GdalReadWindow {
149
    fn gdal_window_start(&self) -> (isize, isize) {
5✔
150
        (self.read_start_x, self.read_start_y)
5✔
151
    }
152

153
    fn gdal_window_size(&self) -> (usize, usize) {
5✔
154
        (self.read_size_x, self.read_size_y)
5✔
155
    }
156
}
157

158
/// A user friendly representation of Gdal's geo transform. In contrast to [`GeoTransform`] this
159
/// geo transform allows arbitrary pixel sizes and can thus also represent rasters where the origin is not located
160
/// in the upper left corner. It should only be used for loading rasters with Gdal and not internally.
161
/// 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.
162
/// However, there are datasets where the data is stored "upside-down". If this is the case, the pixel size is positive.
163
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
164
#[serde(rename_all = "camelCase")]
165
pub struct GdalDatasetGeoTransform {
166
    pub origin_coordinate: Coordinate2D,
167
    pub x_pixel_size: f64,
168
    pub y_pixel_size: f64,
169
}
170

171
impl GdalDatasetGeoTransform {
172
    /// Produce the `SpatialPartition` anchored at the datasets origin with a size of x * y pixels. This method handles non-standard pixel sizes.
173
    pub fn spatial_partition(&self, x_size: usize, y_size: usize) -> SpatialPartition2D {
2✔
174
        // the opposite y value (y value of the non origin edge)
175
        let opposite_coord_y = self.origin_coordinate.y + self.y_pixel_size * y_size as f64;
2✔
176

177
        // if the y-axis is negative then the origin is on the upper side.
178
        let (upper_y, lower_y) = if self.y_pixel_size.is_sign_negative() {
5✔
179
            (self.origin_coordinate.y, opposite_coord_y)
2✔
180
        } else {
181
            (opposite_coord_y, self.origin_coordinate.y)
1✔
182
        };
183

184
        let opposite_coord_x = self.origin_coordinate.x + self.x_pixel_size * x_size as f64;
2✔
185

186
        // if the y-axis is negative then the origin is on the upper side.
187
        let (left_x, right_x) = if self.x_pixel_size.is_sign_positive() {
4✔
188
            (self.origin_coordinate.x, opposite_coord_x)
2✔
189
        } else {
190
            (opposite_coord_x, self.origin_coordinate.x)
×
191
        };
192

193
        SpatialPartition2D::new_unchecked(
194
            Coordinate2D::new(left_x, upper_y),
2✔
195
            Coordinate2D::new(right_x, lower_y),
2✔
196
        )
197
    }
198

199
    /// Transform a `Coordinate2D` into a `GridIdx2D`
200
    #[inline]
201
    pub fn coordinate_to_grid_idx_2d(&self, coord: Coordinate2D) -> GridIdx2D {
5✔
202
        // TODO: use an epsilon here?
203
        let grid_x_index =
7✔
204
            ((coord.x - self.origin_coordinate.x) / self.x_pixel_size).floor() as isize;
×
205
        let grid_y_index =
5✔
206
            ((coord.y - self.origin_coordinate.y) / self.y_pixel_size).floor() as isize;
×
207

208
        [grid_y_index, grid_x_index].into()
7✔
209
    }
210

211
    fn spatial_partition_to_read_window(
7✔
212
        &self,
213
        spatial_partition: &SpatialPartition2D,
214
    ) -> GdalReadWindow {
215
        // 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`].
216
        const EPSILON: f64 = 0.000_001;
217
        let epsilon: Coordinate2D =
5✔
218
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
219

220
        /*
221
        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:
222

223
        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.
224
        ul                      ur
225
        +_______________________+
226
        |_|_ row 1              |
227
        | |_|_  row 2           |
228
        |   |_|_  row ...       |
229
        |     |_|               |
230
        |_______________________|
231
        +                       *
232
        ll                      lr
233

234
        However, sometimes the data is stored up-side down. Like this:
235

236
        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.
237

238
        ul                      ur
239
        +_______________________+
240
        |      _                |
241
        |    _|_|  row ...      |
242
        |  _|_|  row 3          |
243
        | |_|  row 2            |
244
        |_______________________|
245
        +                       *
246
        ll                      lr
247

248
        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.
249
        */
250
        let (near_origin_coord, far_origin_coord) = if self.y_pixel_size.is_sign_negative() {
13✔
251
            (
252
                spatial_partition.upper_left(),
5✔
253
                spatial_partition.lower_right(),
7✔
254
            )
255
        } else {
256
            (
257
                spatial_partition.lower_left(),
1✔
258
                spatial_partition.upper_right(),
1✔
259
            )
260
        };
261

262
        // Move the coordinate near the origin a bit inside the bbox by adding an epsilon of the pixel size.
263
        let safe_near_coord = near_origin_coord + epsilon;
7✔
264
        // Move the coordinate far from the origin a bit inside the bbox by subtracting an epsilon of the pixel size
265
        let safe_far_coord = far_origin_coord - epsilon;
5✔
266

267
        let GridIdx([near_idx_y, near_idx_x]) = self.coordinate_to_grid_idx_2d(safe_near_coord);
7✔
268
        let GridIdx([far_idx_y, far_idx_x]) = self.coordinate_to_grid_idx_2d(safe_far_coord);
5✔
269

270
        debug_assert!(near_idx_x <= far_idx_x);
7✔
271
        debug_assert!(near_idx_y <= far_idx_y);
5✔
272

273
        let read_size_x = (far_idx_x - near_idx_x) as usize + 1;
12✔
274
        let read_size_y = (far_idx_y - near_idx_y) as usize + 1;
12✔
275

276
        GdalReadWindow {
277
            read_start_x: near_idx_x,
278
            read_start_y: near_idx_y,
279
            read_size_x: read_size_x as usize,
280
            read_size_y: read_size_y as usize,
281
        }
282
    }
283
}
284

285
/// Default implementation for testing purposes where geo transform doesn't matter
286
impl TestDefault for GdalDatasetGeoTransform {
287
    fn test_default() -> Self {
1✔
288
        Self {
289
            origin_coordinate: (0.0, 0.0).into(),
1✔
290
            x_pixel_size: 1.0,
291
            y_pixel_size: -1.0,
292
        }
293
    }
294
}
295

296
impl ApproxEq for GdalDatasetGeoTransform {
297
    type Margin = float_cmp::F64Margin;
298

299
    fn approx_eq<M>(self, other: Self, margin: M) -> bool
7✔
300
    where
301
        M: Into<Self::Margin>,
302
    {
303
        let m = margin.into();
5✔
304
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
21✔
305
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
5✔
306
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
5✔
307
    }
308
}
309

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

314
    fn try_from(dataset_geo_transform: GdalDatasetGeoTransform) -> Result<Self> {
×
315
        ensure!(
×
316
            dataset_geo_transform.x_pixel_size > 0.0 && dataset_geo_transform.y_pixel_size < 0.0,
×
317
            crate::error::GeoTransformOrigin
318
        );
319

320
        Ok(GeoTransform::new(
×
321
            dataset_geo_transform.origin_coordinate,
×
322
            dataset_geo_transform.x_pixel_size,
×
323
            dataset_geo_transform.y_pixel_size,
×
324
        ))
325
    }
326
}
327

328
impl From<gdal::GeoTransform> for GdalDatasetGeoTransform {
329
    fn from(gdal_geo_transform: gdal::GeoTransform) -> Self {
5✔
330
        Self {
331
            origin_coordinate: (gdal_geo_transform[0], gdal_geo_transform[3]).into(),
5✔
332
            x_pixel_size: gdal_geo_transform[1],
5✔
333
            y_pixel_size: gdal_geo_transform[5],
5✔
334
        }
335
    }
336
}
337

338
impl SpatialPartitioned for GdalDatasetParameters {
339
    fn spatial_partition(&self) -> SpatialPartition2D {
2✔
340
        self.geo_transform
4✔
341
            .spatial_partition(self.width, self.height)
2✔
342
    }
343
}
344

345
impl GridShapeAccess for GdalDatasetParameters {
346
    type ShapeArray = [usize; 2];
347

348
    fn grid_shape_array(&self) -> Self::ShapeArray {
×
349
        [self.height, self.width]
×
350
    }
351
}
352

353
/// How to handle file not found errors
354
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
355
pub enum FileNotFoundHandling {
356
    NoData, // output tiles filled with nodata
357
    Error,  // return error tile
358
}
359

360
impl GdalDatasetParameters {
361
    /// Placeholders are replaced by formatted time value.
362
    /// E.g. `%my_placeholder%` could be replaced by `2014-04-01` depending on the format and time input.
363
    pub fn replace_time_placeholders(
2✔
364
        &self,
365
        placeholders: &HashMap<String, GdalSourceTimePlaceholder>,
366
        time: TimeInterval,
367
    ) -> Result<Self> {
368
        let mut file_path: String = self.file_path.to_string_lossy().into();
2✔
369

370
        for (placeholder, time_placeholder) in placeholders {
6✔
371
            let time = match time_placeholder.reference {
2✔
372
                TimeReference::Start => time.start(),
4✔
373
                TimeReference::End => time.end(),
×
374
            };
375
            let time_string = time
4✔
376
                .as_date_time()
377
                .ok_or(Error::TimeInstanceNotDisplayable)?
2✔
378
                .format(&time_placeholder.format);
×
379

380
            // TODO: use more efficient algorithm for replacing multiple placeholders, e.g. aho-corasick
381
            file_path = file_path.replace(placeholder, &time_string);
4✔
382
        }
383

384
        Ok(Self {
2✔
385
            file_not_found_handling: self.file_not_found_handling,
2✔
386
            file_path: file_path.into(),
2✔
387
            properties_mapping: self.properties_mapping.clone(),
2✔
388
            gdal_open_options: self.gdal_open_options.clone(),
2✔
389
            gdal_config_options: self.gdal_config_options.clone(),
2✔
390
            ..*self
391
        })
392
    }
393
}
394

395
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
396
pub struct TilingInformation {
397
    pub x_axis_tiles: usize,
398
    pub y_axis_tiles: usize,
399
    pub x_axis_tile_size: usize,
400
    pub y_axis_tile_size: usize,
401
}
402

403
pub struct GdalSourceProcessor<T>
404
where
405
    T: Pixel,
406
{
407
    pub tiling_specification: TilingSpecification,
408
    pub meta_data: GdalMetaData,
409
    pub _phantom_data: PhantomData<T>,
410
}
411

412
struct GdalRasterLoader {}
413

414
impl GdalRasterLoader {
415
    ///
416
    /// A method to async load single tiles from a GDAL dataset.
417
    ///
418
    async fn load_tile_data_async<T: Pixel + GdalType + FromPrimitive>(
3✔
419
        dataset_params: GdalDatasetParameters,
420
        tile_information: TileInformation,
421
        tile_time: TimeInterval,
422
    ) -> Result<RasterTile2D<T>> {
423
        crate::util::spawn_blocking(move || {
17✔
424
            Self::load_tile_data(&dataset_params, tile_information, tile_time)
3✔
425
        })
426
        .await
13✔
427
        .context(crate::error::TokioJoin)?
×
428
    }
429

430
    async fn load_tile_async<T: Pixel + GdalType + FromPrimitive>(
4✔
431
        dataset_params: Option<GdalDatasetParameters>,
432
        tile_information: TileInformation,
433
        tile_time: TimeInterval,
434
    ) -> Result<RasterTile2D<T>> {
435
        match dataset_params {
4✔
436
            // 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.
437
            Some(ds)
6✔
438
                if tile_information
6✔
439
                    .spatial_partition()
×
440
                    .intersects(&ds.spatial_partition()) =>
3✔
441
            {
442
                debug!(
9✔
443
                    "Loading tile {:?}, from {:?}, band: {}",
444
                    &tile_information, ds.file_path, ds.rasterband_channel
×
445
                );
446
                Self::load_tile_data_async(ds, tile_information, tile_time).await
7✔
447
            }
448
            Some(_) => {
×
449
                debug!("Skipping tile not in query rect {:?}", &tile_information);
9✔
450

451
                Ok(create_no_data_tile(tile_information, tile_time))
3✔
452
            }
453

454
            _ => {
×
455
                debug!(
6✔
456
                    "Skipping tile without GdalDatasetParameters {:?}",
457
                    &tile_information
×
458
                );
459

460
                Ok(create_no_data_tile(tile_information, tile_time))
2✔
461
            }
462
        }
463
    }
464

465
    ///
466
    /// A method to load single tiles from a GDAL dataset.
467
    ///
468
    fn load_tile_data<T: Pixel + GdalType + FromPrimitive>(
5✔
469
        dataset_params: &GdalDatasetParameters,
470
        tile_information: TileInformation,
471
        tile_time: TimeInterval,
472
    ) -> Result<RasterTile2D<T>> {
473
        let start = Instant::now();
5✔
474

475
        debug!(
14✔
476
            "GridOrEmpty2D<{:?}> requested for {:?}.",
477
            T::TYPE,
×
478
            &tile_information.spatial_partition()
×
479
        );
480

481
        let options = dataset_params
14✔
482
            .gdal_open_options
×
483
            .as_ref()
484
            .map(|o| o.iter().map(String::as_str).collect::<Vec<_>>());
×
485

486
        // reverts the thread local configs on drop
487
        let _thread_local_configs = dataset_params
14✔
488
            .gdal_config_options
×
489
            .as_ref()
490
            .map(|config_options| TemporaryGdalThreadLocalConfigOptions::new(config_options));
2✔
491

492
        let dataset_result = gdal_open_dataset_ex(
493
            &dataset_params.file_path,
8✔
494
            DatasetOptions {
6✔
495
                open_flags: GdalOpenFlags::GDAL_OF_RASTER,
×
496
                open_options: options.as_deref(),
6✔
497
                ..DatasetOptions::default()
8✔
498
            },
499
        );
500

501
        if dataset_result.is_err() {
14✔
502
            // TODO: check if Gdal error is actually file not found
503

504
            let err_result = match dataset_params.file_not_found_handling {
×
505
                FileNotFoundHandling::NoData => {
×
506
                    Ok(create_no_data_tile(tile_information, tile_time))
×
507
                }
508
                FileNotFoundHandling::Error => Err(crate::error::Error::CouldNotOpenGdalDataset {
×
509
                    file_path: dataset_params.file_path.to_string_lossy().to_string(),
×
510
                }),
511
            };
512
            let elapsed = start.elapsed();
×
513
            debug!(
×
514
                "file not found -> returning error = {}, took: {:?}, file: {:?}",
515
                err_result.is_err(),
×
516
                elapsed,
×
517
                dataset_params.file_path
×
518
            );
519
            return err_result;
×
520
        };
521

522
        let dataset = dataset_result.expect("checked");
14✔
523

524
        let result_tile = read_raster_tile_with_properties(
525
            &dataset,
×
526
            dataset_params,
×
527
            tile_information,
8✔
528
            tile_time,
6✔
529
        )?;
530

531
        let elapsed = start.elapsed();
12✔
532
        debug!("data loaded -> returning data grid, took {:?}", elapsed);
12✔
533

534
        Ok(result_tile)
6✔
535
    }
536

537
    ///
538
    /// A stream of futures producing `RasterTile2D` for a single slice in time
539
    ///
540
    fn temporal_slice_tile_future_stream<T: Pixel + GdalType + FromPrimitive>(
3✔
541
        query: RasterQueryRectangle,
542
        info: GdalLoadingInfoTemporalSlice,
543
        tiling_strategy: TilingStrategy,
544
    ) -> impl Stream<Item = impl Future<Output = Result<RasterTile2D<T>>>> {
545
        stream::iter(tiling_strategy.tile_information_iterator(query.spatial_bounds)).map(
9✔
546
            move |tile| GdalRasterLoader::load_tile_async(info.params.clone(), tile, info.time),
9✔
547
        )
548
    }
549

550
    fn loading_info_to_tile_stream<
3✔
551
        T: Pixel + GdalType + FromPrimitive,
552
        S: Stream<Item = Result<GdalLoadingInfoTemporalSlice>>,
553
    >(
554
        loading_info_stream: S,
555
        query: RasterQueryRectangle,
556
        tiling_strategy: TilingStrategy,
557
    ) -> impl Stream<Item = Result<RasterTile2D<T>>> {
558
        loading_info_stream
6✔
559
            .map_ok(move |info| {
6✔
560
                GdalRasterLoader::temporal_slice_tile_future_stream(query, info, tiling_strategy)
3✔
561
                    .map(Result::Ok)
×
562
            })
563
            .try_flatten()
564
            .try_buffered(16) // TODO: make this configurable
565
    }
566
}
567

568
impl<T> GdalSourceProcessor<T> where T: gdal::raster::GdalType + Pixel {}
569

570
#[async_trait]
571
impl<P> QueryProcessor for GdalSourceProcessor<P>
572
where
573
    P: Pixel + gdal::raster::GdalType + FromPrimitive,
574
{
575
    type Output = RasterTile2D<P>;
576
    type SpatialBounds = SpatialPartition2D;
577

578
    async fn _query<'a>(
3✔
579
        &'a self,
580
        query: RasterQueryRectangle,
581
        _ctx: &'a dyn crate::engine::QueryContext,
582
    ) -> Result<BoxStream<Result<Self::Output>>> {
583
        let start = Instant::now();
3✔
584
        debug!(
6✔
585
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
586
            P::TYPE,
×
587
            &query
×
588
        );
589

590
        debug!(
6✔
591
            "GdalSourceProcessor<{:?}> meta data loaded, took {:?}.",
592
            P::TYPE,
×
593
            start.elapsed()
×
594
        );
595

596
        let spatial_resolution = query.spatial_resolution;
3✔
597

598
        // A `GeoTransform` maps pixel space to world space.
599
        // Usually a SRS has axis directions pointing "up" (y-axis) and "up" (y-axis).
600
        // We are not aware of spatial reference systems where the x-axis points to the right.
601
        // However, there are spatial reference systems where the y-axis points downwards.
602
        // The standard "pixel-space" starts at the top-left corner of a `GeoTransform` and points down-right.
603
        // Therefore, the pixel size on the x-axis is always increasing
604
        let pixel_size_x = spatial_resolution.x;
3✔
605
        debug_assert!(pixel_size_x.is_sign_positive());
3✔
606
        // and the y-axis should only be positive if the y-axis of the spatial reference system also "points down".
607
        // NOTE: at the moment we do not allow "down pointing" y-axis.
608
        let pixel_size_y = spatial_resolution.y * -1.0;
3✔
609
        debug_assert!(pixel_size_y.is_sign_negative());
3✔
610

611
        let tiling_strategy = self
3✔
612
            .tiling_specification
×
613
            .strategy(pixel_size_x, pixel_size_y);
×
614

615
        let result_descriptor = self.meta_data.result_descriptor().await?;
6✔
616

617
        let mut empty = false;
3✔
618
        debug!("result descr bbox: {:?}", result_descriptor.bbox);
9✔
619
        debug!("query bbox: {:?}", query.spatial_bounds);
6✔
620

621
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
6✔
622
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
4✔
623
                debug!("query does not intersect spatial data bounds");
×
624
                empty = true;
×
625
            }
626
        }
627

628
        // TODO: use the time bounds to early return.
629
        /*
630
        if let Some(data_time_bounds) = result_descriptor.time {
631
            if !data_time_bounds.intersects(&query.time_interval) {
632
                debug!("query does not intersect temporal data bounds");
633
                empty = true;
634
            }
635
        }
636
        */
637

638
        let loading_iter = if empty {
6✔
639
            GdalLoadingInfoTemporalSliceIterator::Static {
640
                parts: vec![].into_iter(),
×
641
            }
642
        } else {
643
            let loading_info = self.meta_data.loading_info(query).await?;
7✔
644
            loading_info.info
3✔
645
        };
646

647
        let source_stream = stream::iter(loading_iter);
3✔
648

649
        let source_stream =
3✔
650
            GdalRasterLoader::loading_info_to_tile_stream(source_stream, query, tiling_strategy);
×
651

652
        // use SparseTilesFillAdapter to fill all the gaps
653
        let filled_stream = SparseTilesFillAdapter::new(
654
            source_stream,
3✔
655
            tiling_strategy.tile_grid_box(query.spatial_partition()),
6✔
656
            tiling_strategy.geo_transform,
3✔
657
            tiling_strategy.tile_size_in_pixels,
3✔
658
        );
659
        Ok(filled_stream.boxed())
6✔
660
    }
661
}
662

663
pub type GdalSource = SourceOperator<GdalSourceParameters>;
664

665
impl OperatorName for GdalSource {
666
    const TYPE_NAME: &'static str = "GdalSource";
667
}
668

669
#[typetag::serde]
670
#[async_trait]
671
impl RasterOperator for GdalSource {
672
    async fn _initialize(
2✔
673
        self: Box<Self>,
674
        context: &dyn crate::engine::ExecutionContext,
675
    ) -> Result<Box<dyn InitializedRasterOperator>> {
676
        let meta_data: GdalMetaData = context.meta_data(&self.params.data).await?;
6✔
677

678
        debug!("Initializing GdalSource for {:?}.", &self.params.data);
6✔
679

680
        let op = InitializedGdalSourceOperator {
681
            result_descriptor: meta_data.result_descriptor().await?,
4✔
682
            meta_data,
683
            tiling_specification: context.tiling_specification(),
2✔
684
        };
685

686
        Ok(op.boxed())
4✔
687
    }
688

689
    span_fn!(GdalSource);
690
}
691

692
pub struct InitializedGdalSourceOperator {
693
    pub meta_data: GdalMetaData,
694
    pub result_descriptor: RasterResultDescriptor,
695
    pub tiling_specification: TilingSpecification,
696
}
697

698
impl InitializedRasterOperator for InitializedGdalSourceOperator {
699
    fn result_descriptor(&self) -> &RasterResultDescriptor {
2✔
700
        &self.result_descriptor
2✔
701
    }
702

703
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
2✔
704
        Ok(match self.result_descriptor().data_type {
4✔
705
            RasterDataType::U8 => TypedRasterQueryProcessor::U8(
2✔
706
                GdalSourceProcessor {
2✔
707
                    tiling_specification: self.tiling_specification,
2✔
708
                    meta_data: self.meta_data.clone(),
2✔
709
                    _phantom_data: PhantomData,
710
                }
711
                .boxed(),
712
            ),
713
            RasterDataType::U16 => TypedRasterQueryProcessor::U16(
1✔
714
                GdalSourceProcessor {
1✔
715
                    tiling_specification: self.tiling_specification,
1✔
716
                    meta_data: self.meta_data.clone(),
1✔
717
                    _phantom_data: PhantomData,
718
                }
719
                .boxed(),
720
            ),
721
            RasterDataType::U32 => TypedRasterQueryProcessor::U32(
×
722
                GdalSourceProcessor {
×
723
                    tiling_specification: self.tiling_specification,
×
724
                    meta_data: self.meta_data.clone(),
×
725
                    _phantom_data: PhantomData,
726
                }
727
                .boxed(),
728
            ),
729
            RasterDataType::U64 => {
730
                return Err(GdalSourceError::UnsupportedRasterType {
×
731
                    raster_type: RasterDataType::U64,
732
                })?
733
            }
734
            RasterDataType::I8 => {
735
                return Err(GdalSourceError::UnsupportedRasterType {
×
736
                    raster_type: RasterDataType::I8,
737
                })?
738
            }
739
            RasterDataType::I16 => TypedRasterQueryProcessor::I16(
×
740
                GdalSourceProcessor {
×
741
                    tiling_specification: self.tiling_specification,
×
742
                    meta_data: self.meta_data.clone(),
×
743
                    _phantom_data: PhantomData,
744
                }
745
                .boxed(),
746
            ),
747
            RasterDataType::I32 => TypedRasterQueryProcessor::I32(
×
748
                GdalSourceProcessor {
×
749
                    tiling_specification: self.tiling_specification,
×
750
                    meta_data: self.meta_data.clone(),
×
751
                    _phantom_data: PhantomData,
752
                }
753
                .boxed(),
754
            ),
755
            RasterDataType::I64 => {
756
                return Err(GdalSourceError::UnsupportedRasterType {
×
757
                    raster_type: RasterDataType::I64,
758
                })?
759
            }
760
            RasterDataType::F32 => TypedRasterQueryProcessor::F32(
×
761
                GdalSourceProcessor {
×
762
                    tiling_specification: self.tiling_specification,
×
763
                    meta_data: self.meta_data.clone(),
×
764
                    _phantom_data: PhantomData,
765
                }
766
                .boxed(),
767
            ),
768
            RasterDataType::F64 => TypedRasterQueryProcessor::F64(
×
769
                GdalSourceProcessor {
×
770
                    tiling_specification: self.tiling_specification,
×
771
                    meta_data: self.meta_data.clone(),
×
772
                    _phantom_data: PhantomData,
773
                }
774
                .boxed(),
775
            ),
776
        })
777
    }
778
}
779

780
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
781
/// It fails if the tile is not within the dataset.
782
#[allow(clippy::float_cmp)]
783
fn read_grid_from_raster<T, D>(
9✔
784
    rasterband: &GdalRasterBand,
785
    read_window: &GdalReadWindow,
786
    out_shape: D,
787
    dataset_params: &GdalDatasetParameters,
788
    flip_y_axis: bool,
789
) -> Result<GridOrEmpty<D, T>>
790
where
791
    T: Pixel + GdalType + Default + FromPrimitive,
792
    D: Clone + GridSize + PartialEq,
793
{
794
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
18✔
795

796
    let buffer = rasterband.read_as::<T>(
9✔
797
        read_window.gdal_window_start(), // pixelspace origin
9✔
798
        read_window.gdal_window_size(),  // pixelspace size
9✔
799
        gdal_out_shape,                  // requested raster size
9✔
800
        None,                            // sampling mode
9✔
801
    )?;
802
    let data_grid = Grid::new(out_shape.clone(), buffer.data)?;
18✔
803

804
    let data_grid = if flip_y_axis {
18✔
805
        data_grid.reversed_y_axis_grid()
2✔
806
    } else {
807
        data_grid
9✔
808
    };
809

810
    let dataset_mask_flags = rasterband.mask_flags()?;
18✔
811

812
    if dataset_mask_flags.is_all_valid() {
18✔
813
        debug!("all pixels are valid --> skip no-data and mask handling.");
2✔
814
        return Ok(MaskedGrid::new_with_data(data_grid).into());
2✔
815
    }
816

817
    if dataset_mask_flags.is_nodata() {
18✔
818
        debug!("raster uses a no-data value --> use no-data handling.");
18✔
819
        let no_data_value = dataset_params
27✔
820
            .no_data_value
×
821
            .or_else(|| rasterband.no_data_value())
9✔
822
            .and_then(FromPrimitive::from_f64);
×
823
        let no_data_value_grid = NoDataValueGrid::new(data_grid, no_data_value);
9✔
824
        let grid_or_empty = GridOrEmpty::from(no_data_value_grid);
9✔
825
        return Ok(grid_or_empty);
9✔
826
    }
827

828
    if dataset_mask_flags.is_alpha() {
8✔
829
        debug!("raster uses alpha band to mask pixels.");
×
830
        if !dataset_params.allow_alphaband_as_mask {
×
831
            return Err(Error::AlphaBandAsMaskNotAllowed);
×
832
        }
833
    }
834

835
    debug!("use mask based no-data handling.");
12✔
836

837
    let mask_band = rasterband.open_mask_band()?;
8✔
838
    let mask_buffer = mask_band.read_as::<u8>(
4✔
839
        read_window.gdal_window_start(), // pixelspace origin
4✔
840
        read_window.gdal_window_size(),  // pixelspace size
4✔
841
        gdal_out_shape,                  // requested raster size
4✔
842
        None,                            // sampling mode
4✔
843
    )?;
844
    let mask_grid = Grid::new(out_shape, mask_buffer.data)?.map_elements(|p: u8| p > 0);
20✔
845

846
    let mask_grid = if flip_y_axis {
6✔
847
        mask_grid.reversed_y_axis_grid()
×
848
    } else {
849
        mask_grid
1✔
850
    };
851

852
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
1✔
853
    Ok(GridOrEmpty::from(masked_grid))
4✔
854
}
855

856
/// This method reads the data for a single grid with a specified size from the GDAL dataset.
857
/// If the tile overlaps the borders of the dataset only the data in the dataset bounds is read.
858
/// The data read from the dataset is clipped into a grid with the requested size filled  with the `no_data_value`.
859
fn read_partial_grid_from_raster<T>(
7✔
860
    rasterband: &GdalRasterBand,
861
    gdal_read_window: &GdalReadWindow,
862
    out_tile_read_bounds: GridBoundingBox2D,
863
    out_tile_shape: GridShape2D,
864
    dataset_params: &GdalDatasetParameters,
865
    flip_y_axis: bool,
866
) -> Result<GridOrEmpty2D<T>>
867
where
868
    T: Pixel + GdalType + Default + FromPrimitive,
869
{
870
    let dataset_raster = read_grid_from_raster(
871
        rasterband,
×
872
        gdal_read_window,
×
873
        out_tile_read_bounds,
7✔
874
        dataset_params,
×
875
        flip_y_axis,
×
876
    )?;
877

878
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
14✔
879
    tile_raster.grid_blit_from(&dataset_raster);
7✔
880
    Ok(tile_raster)
7✔
881
}
882

883
/// This method reads the data for a single tile with a specified size from the GDAL dataset.
884
/// It handles conversion to grid coordinates.
885
/// If the tile is inside the dataset it uses the `read_grid_from_raster` method.
886
/// If the tile overlaps the borders of the dataset it uses the `read_partial_grid_from_raster` method.  
887
fn read_grid_and_handle_edges<T>(
6✔
888
    tile_info: TileInformation,
889
    dataset: &GdalDataset,
890
    rasterband: &GdalRasterBand,
891
    dataset_params: &GdalDatasetParameters,
892
) -> Result<GridOrEmpty2D<T>>
893
where
894
    T: Pixel + GdalType + Default + FromPrimitive,
895
{
896
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
8✔
897
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
6✔
898

899
    if !approx_eq!(
14✔
900
        GdalDatasetGeoTransform,
×
901
        gdal_dataset_geotransform,
6✔
902
        dataset_params.geo_transform
8✔
903
    ) {
904
        log::warn!(
×
905
            "GdalDatasetParameters geo transform is different to the one retrieved from GDAL dataset: {:?} != {:?}",
906
            dataset_params.geo_transform,
×
907
            gdal_dataset_geotransform,
×
908
        );
909
    };
910

911
    debug_assert_eq!(gdal_dataset_pixels_x, dataset_params.width);
6✔
912
    debug_assert_eq!(gdal_dataset_pixels_y, dataset_params.height);
8✔
913

914
    let gdal_dataset_bounds =
6✔
915
        gdal_dataset_geotransform.spatial_partition(gdal_dataset_pixels_x, gdal_dataset_pixels_y);
×
916

917
    let output_bounds = tile_info.spatial_partition();
8✔
918
    let dataset_intersects_tile = gdal_dataset_bounds.intersection(&output_bounds);
6✔
919
    let output_shape = tile_info.tile_size_in_pixels();
6✔
920

921
    let dataset_intersection_area = match dataset_intersects_tile {
8✔
922
        Some(i) => i,
6✔
923
        None => {
×
924
            return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
925
        }
926
    };
927

928
    let tile_geo_transform = tile_info.tile_geo_transform();
8✔
929

930
    let gdal_read_window =
6✔
931
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
×
932

933
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
16✔
934
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
6✔
935

936
    if is_y_axis_flipped {
6✔
937
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
2✔
938
    }
939

940
    let result_grid = if dataset_intersection_area == output_bounds {
23✔
941
        read_grid_from_raster(
942
            rasterband,
×
943
            &gdal_read_window,
×
944
            output_shape,
2✔
945
            dataset_params,
×
946
            is_y_axis_flipped,
×
947
        )?
948
    } else {
949
        let partial_tile_grid_bounds =
7✔
950
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
×
951

952
        read_partial_grid_from_raster(
953
            rasterband,
×
954
            &gdal_read_window,
×
955
            partial_tile_grid_bounds,
7✔
956
            output_shape,
7✔
957
            dataset_params,
×
958
            is_y_axis_flipped,
×
959
        )?
960
    };
961

962
    Ok(result_grid)
6✔
963
}
964

965
/// 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.
966
fn read_raster_tile_with_properties<T: Pixel + gdal::raster::GdalType + FromPrimitive>(
8✔
967
    dataset: &GdalDataset,
968
    dataset_params: &GdalDatasetParameters,
969
    tile_info: TileInformation,
970
    tile_time: TimeInterval,
971
) -> Result<RasterTile2D<T>> {
972
    let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
6✔
973

974
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
14✔
975

976
    let mut properties = RasterProperties::default();
6✔
977

978
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
12✔
979
        properties_from_gdal(&mut properties, dataset, properties_mapping);
1✔
980
        let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
1✔
981
        properties_from_gdal(&mut properties, &rasterband, properties_mapping);
1✔
982
        properties_from_band(&mut properties, &rasterband);
1✔
983
    }
984

985
    Ok(RasterTile2D::new_with_tile_info_and_properties(
12✔
986
        tile_time,
6✔
987
        tile_info,
6✔
988
        result_grid,
6✔
989
        properties,
6✔
990
    ))
991
}
992

993
fn create_no_data_tile<T: Pixel>(
4✔
994
    tile_info: TileInformation,
995
    tile_time: TimeInterval,
996
) -> RasterTile2D<T> {
997
    RasterTile2D::new_with_tile_info_and_properties(
998
        tile_time,
4✔
999
        tile_info,
4✔
1000
        EmptyGrid::new(tile_info.tile_size_in_pixels).into(),
4✔
1001
        RasterProperties::default(),
4✔
1002
    )
1003
}
1004

1005
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1006
pub struct GdalMetadataMapping {
1007
    pub source_key: RasterPropertiesKey,
1008
    pub target_key: RasterPropertiesKey,
1009
    pub target_type: RasterPropertiesEntryType,
1010
}
1011

1012
impl GdalMetadataMapping {
1013
    pub fn identity(
×
1014
        key: RasterPropertiesKey,
1015
        target_type: RasterPropertiesEntryType,
1016
    ) -> GdalMetadataMapping {
1017
        GdalMetadataMapping {
1018
            source_key: key.clone(),
×
1019
            target_key: key,
1020
            target_type,
1021
        }
1022
    }
1023
}
1024

1025
fn properties_from_gdal<'a, I, M>(
2✔
1026
    properties: &mut RasterProperties,
1027
    gdal_dataset: &M,
1028
    properties_mapping: I,
1029
) where
1030
    I: IntoIterator<Item = &'a GdalMetadataMapping>,
1031
    M: GdalMetadata,
1032
{
1033
    let mapping_iter = properties_mapping.into_iter();
2✔
1034

1035
    for m in mapping_iter {
4✔
1036
        let data = if let Some(domain) = &m.source_key.domain {
4✔
1037
            gdal_dataset.metadata_item(&m.source_key.key, domain)
4✔
1038
        } else {
1039
            gdal_dataset.metadata_item(&m.source_key.key, "")
2✔
1040
        };
1041

1042
        if let Some(d) = data {
4✔
1043
            let entry = match m.target_type {
1✔
1044
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1045
                    |_| RasterPropertiesEntry::String(d),
×
1046
                    RasterPropertiesEntry::Number,
×
1047
                ),
1048
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
1✔
1049
            };
1050

1051
            debug!(
3✔
1052
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
1053
                &m.source_key, &m.target_key, &entry
×
1054
            );
1055

1056
            properties
2✔
1057
                .properties_map
×
1058
                .insert(m.target_key.clone(), entry);
3✔
1059
        }
1060
    }
1061
}
1062

1063
fn properties_from_band(properties: &mut RasterProperties, gdal_dataset: &GdalRasterBand) {
1✔
1064
    if let Some(scale) = gdal_dataset.metadata_item("scale", "") {
1✔
1065
        properties.scale = scale.parse::<f64>().ok();
×
1066
    };
1067

1068
    if let Some(offset) = gdal_dataset.metadata_item("offset", "") {
2✔
1069
        properties.offset = offset.parse::<f64>().ok();
×
1070
    };
1071

1072
    if let Some(band_name) = gdal_dataset.metadata_item("band_name", "") {
2✔
1073
        properties.band_name = Some(band_name);
×
1074
    }
1075
}
1076

1077
#[cfg(test)]
1078
mod tests {
1079
    use super::*;
1080
    use crate::engine::{MockExecutionContext, MockQueryContext};
1081
    use crate::test_data;
1082
    use crate::util::gdal::add_ndvi_dataset;
1083
    use crate::util::Result;
1084

1085
    use geoengine_datatypes::hashmap;
1086
    use geoengine_datatypes::primitives::{AxisAlignedRectangle, SpatialPartition2D, TimeInstance};
1087
    use geoengine_datatypes::raster::{EmptyGrid2D, GridBounds, GridIdx2D};
1088
    use geoengine_datatypes::raster::{TileInformation, TilingStrategy};
1089
    use geoengine_datatypes::util::gdal::hide_gdal_errors;
1090
    use geoengine_datatypes::{primitives::SpatialResolution, raster::GridShape2D};
1091

1092
    async fn query_gdal_source(
1✔
1093
        exe_ctx: &MockExecutionContext,
1094
        query_ctx: &MockQueryContext,
1095
        id: DataId,
1096
        output_shape: GridShape2D,
1097
        output_bounds: SpatialPartition2D,
1098
        time_interval: TimeInterval,
1099
    ) -> Vec<Result<RasterTile2D<u8>>> {
1100
        let op = GdalSource {
1101
            params: GdalSourceParameters { data: id.clone() },
2✔
1102
        }
1103
        .boxed();
1104

1105
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
2✔
1106
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
1✔
1107
        let spatial_resolution =
1✔
1108
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
1109

1110
        let o = op.initialize(exe_ctx).await.unwrap();
2✔
1111

1112
        o.query_processor()
8✔
1113
            .unwrap()
1114
            .get_u8()
1115
            .unwrap()
1116
            .raster_query(
1117
                RasterQueryRectangle {
1✔
1118
                    spatial_bounds: output_bounds,
1✔
1119
                    time_interval,
1✔
1120
                    spatial_resolution,
1✔
1121
                },
1122
                query_ctx,
1✔
1123
            )
1124
            .await
4✔
1125
            .unwrap()
1126
            .collect()
1127
            .await
4✔
1128
    }
1129

1130
    fn load_ndvi_jan_2014(
1✔
1131
        output_shape: GridShape2D,
1132
        output_bounds: SpatialPartition2D,
1133
    ) -> Result<RasterTile2D<u8>> {
1134
        GdalRasterLoader::load_tile_data::<u8>(
1135
            &GdalDatasetParameters {
1✔
1136
                file_path: test_data!("raster/modis_ndvi/MOD13A2_M_NDVI_2014-01-01.TIFF").into(),
4✔
1137
                rasterband_channel: 1,
1138
                geo_transform: GdalDatasetGeoTransform {
1✔
1139
                    origin_coordinate: (-180., 90.).into(),
1✔
1140
                    x_pixel_size: 0.1,
1141
                    y_pixel_size: -0.1,
1142
                },
1143
                width: 3600,
1144
                height: 1800,
1145
                file_not_found_handling: FileNotFoundHandling::NoData,
1✔
1146
                no_data_value: Some(0.),
1✔
1147
                properties_mapping: Some(vec![
2✔
1148
                    GdalMetadataMapping {
1✔
1149
                        source_key: RasterPropertiesKey {
1✔
1150
                            domain: None,
1✔
1151
                            key: "AREA_OR_POINT".to_string(),
1✔
1152
                        },
1153
                        target_type: RasterPropertiesEntryType::String,
1✔
1154
                        target_key: RasterPropertiesKey {
1✔
1155
                            domain: None,
1✔
1156
                            key: "AREA_OR_POINT".to_string(),
1✔
1157
                        },
1158
                    },
1159
                    GdalMetadataMapping {
1✔
1160
                        source_key: RasterPropertiesKey {
1✔
1161
                            domain: Some("IMAGE_STRUCTURE".to_string()),
2✔
1162
                            key: "COMPRESSION".to_string(),
1✔
1163
                        },
1164
                        target_type: RasterPropertiesEntryType::String,
1✔
1165
                        target_key: RasterPropertiesKey {
1✔
1166
                            domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
2✔
1167
                            key: "COMPRESSION".to_string(),
1✔
1168
                        },
1169
                    },
1170
                ]),
1171
                gdal_open_options: None,
1✔
1172
                gdal_config_options: None,
1✔
1173
                allow_alphaband_as_mask: true,
1174
            },
1175
            TileInformation::with_partition_and_shape(output_bounds, output_shape),
1✔
1176
            TimeInterval::default(),
1✔
1177
        )
1178
    }
1179

1180
    #[test]
1181
    fn tiling_strategy_origin() {
3✔
1182
        let tile_size_in_pixels = [600, 600];
1✔
1183
        let dataset_upper_right_coord = (-180.0, 90.0).into();
1✔
1184
        let dataset_x_pixel_size = 0.1;
1✔
1185
        let dataset_y_pixel_size = -0.1;
1✔
1186
        let dataset_geo_transform = GeoTransform::new(
1187
            dataset_upper_right_coord,
1188
            dataset_x_pixel_size,
1189
            dataset_y_pixel_size,
1190
        );
1191

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

1194
        let origin_split_tileing_strategy = TilingStrategy {
1195
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1196
            geo_transform: dataset_geo_transform,
1197
        };
1198

1199
        assert_eq!(
1✔
1200
            origin_split_tileing_strategy
1✔
1201
                .geo_transform
1202
                .upper_left_pixel_idx(&partition),
1203
            [0, 0].into()
1✔
1204
        );
1205
        assert_eq!(
1✔
1206
            origin_split_tileing_strategy
1✔
1207
                .geo_transform
1208
                .lower_right_pixel_idx(&partition),
1209
            [1800 - 1, 3600 - 1].into()
1✔
1210
        );
1211

1212
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1213
        assert_eq!(tile_grid.axis_size(), [3, 6]);
1✔
1214
        assert_eq!(tile_grid.min_index(), [0, 0].into());
2✔
1215
        assert_eq!(tile_grid.max_index(), [2, 5].into());
2✔
1216
    }
1217

1218
    #[test]
1219
    fn tiling_strategy_zero() {
3✔
1220
        let tile_size_in_pixels = [600, 600];
1✔
1221
        let dataset_x_pixel_size = 0.1;
1✔
1222
        let dataset_y_pixel_size = -0.1;
1✔
1223
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1224
            0.0,
1225
            dataset_x_pixel_size,
1226
            0.0,
1227
            dataset_y_pixel_size,
1228
        );
1229

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

1232
        let origin_split_tileing_strategy = TilingStrategy {
1233
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1234
            geo_transform: central_geo_transform,
1235
        };
1236

1237
        assert_eq!(
1✔
1238
            origin_split_tileing_strategy
1✔
1239
                .geo_transform
1240
                .upper_left_pixel_idx(&partition),
1241
            [-900, -1800].into()
1✔
1242
        );
1243
        assert_eq!(
1✔
1244
            origin_split_tileing_strategy
1✔
1245
                .geo_transform
1246
                .lower_right_pixel_idx(&partition),
1247
            [1800 / 2 - 1, 3600 / 2 - 1].into()
1✔
1248
        );
1249

1250
        let tile_grid = origin_split_tileing_strategy.tile_grid_box(partition);
1✔
1251
        assert_eq!(tile_grid.axis_size(), [4, 6]);
1✔
1252
        assert_eq!(tile_grid.min_index(), [-2, -3].into());
2✔
1253
        assert_eq!(tile_grid.max_index(), [1, 2].into());
2✔
1254
    }
1255

1256
    #[test]
1257
    fn tile_idx_iterator() {
3✔
1258
        let tile_size_in_pixels = [600, 600];
1✔
1259
        let dataset_x_pixel_size = 0.1;
1✔
1260
        let dataset_y_pixel_size = -0.1;
1✔
1261
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1262
            0.0,
1263
            dataset_x_pixel_size,
1264
            0.0,
1265
            dataset_y_pixel_size,
1266
        );
1267

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

1270
        let origin_split_tileing_strategy = TilingStrategy {
1271
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1272
            geo_transform: central_geo_transform,
1273
        };
1274

1275
        let vres: Vec<GridIdx2D> = origin_split_tileing_strategy
1✔
1276
            .tile_idx_iterator(partition)
1✔
1277
            .collect();
1278
        assert_eq!(vres.len(), 4 * 6);
2✔
1279
        assert_eq!(vres[0], [-2, -3].into());
2✔
1280
        assert_eq!(vres[1], [-2, -2].into());
2✔
1281
        assert_eq!(vres[2], [-2, -1].into());
2✔
1282
        assert_eq!(vres[23], [1, 2].into());
2✔
1283
    }
1284

1285
    #[test]
1286
    fn tile_information_iterator() {
3✔
1287
        let tile_size_in_pixels = [600, 600];
1✔
1288
        let dataset_x_pixel_size = 0.1;
1✔
1289
        let dataset_y_pixel_size = -0.1;
1✔
1290

1291
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1292
            0.0,
1293
            dataset_x_pixel_size,
1294
            0.0,
1295
            dataset_y_pixel_size,
1296
        );
1297

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

1300
        let origin_split_tileing_strategy = TilingStrategy {
1301
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1302
            geo_transform: central_geo_transform,
1303
        };
1304

1305
        let vres: Vec<TileInformation> = origin_split_tileing_strategy
1✔
1306
            .tile_information_iterator(partition)
1✔
1307
            .collect();
1308
        assert_eq!(vres.len(), 4 * 6);
2✔
1309
        assert_eq!(
1✔
1310
            vres[0],
1✔
1311
            TileInformation::new(
1✔
1312
                [-2, -3].into(),
1✔
1313
                tile_size_in_pixels.into(),
1✔
1314
                central_geo_transform,
1✔
1315
            )
1316
        );
1317
        assert_eq!(
1✔
1318
            vres[1],
1✔
1319
            TileInformation::new(
1✔
1320
                [-2, -2].into(),
1✔
1321
                tile_size_in_pixels.into(),
1✔
1322
                central_geo_transform,
1✔
1323
            )
1324
        );
1325
        assert_eq!(
1✔
1326
            vres[12],
1✔
1327
            TileInformation::new(
1✔
1328
                [0, -3].into(),
1✔
1329
                tile_size_in_pixels.into(),
1✔
1330
                central_geo_transform,
1✔
1331
            )
1332
        );
1333
        assert_eq!(
1✔
1334
            vres[23],
1✔
1335
            TileInformation::new(
1✔
1336
                [1, 2].into(),
1✔
1337
                tile_size_in_pixels.into(),
1✔
1338
                central_geo_transform,
1✔
1339
            )
1340
        );
1341
    }
1342

1343
    #[test]
1344
    fn replace_time_placeholder() {
3✔
1345
        let params = GdalDatasetParameters {
1346
            file_path: "/foo/bar_%TIME%.tiff".into(),
1✔
1347
            rasterband_channel: 0,
1348
            geo_transform: TestDefault::test_default(),
1✔
1349
            width: 360,
1350
            height: 180,
1351
            file_not_found_handling: FileNotFoundHandling::NoData,
1352
            no_data_value: Some(0.),
1✔
1353
            properties_mapping: None,
1354
            gdal_open_options: None,
1355
            gdal_config_options: None,
1356
            allow_alphaband_as_mask: true,
1357
        };
1358
        let replaced = params
1✔
1359
            .replace_time_placeholders(
1360
                &hashmap! {
3✔
1361
                    "%TIME%".to_string() => GdalSourceTimePlaceholder {
3✔
1362
                        format: DateTimeParseFormat::custom("%f".to_string()),
2✔
1363
                        reference: TimeReference::Start,
1364
                    },
1365
                },
1366
                TimeInterval::new_instant(TimeInstance::from_millis_unchecked(22)).unwrap(),
2✔
1367
            )
1368
            .unwrap();
1369
        assert_eq!(
×
1370
            replaced.file_path.to_string_lossy(),
1✔
1371
            "/foo/bar_022000000.tiff".to_string()
1✔
1372
        );
1373
        assert_eq!(params.rasterband_channel, replaced.rasterband_channel);
1✔
1374
        assert_eq!(params.geo_transform, replaced.geo_transform);
2✔
1375
        assert_eq!(params.width, replaced.width);
1✔
1376
        assert_eq!(params.height, replaced.height);
1✔
1377
        assert_eq!(
2✔
1378
            params.file_not_found_handling,
1379
            replaced.file_not_found_handling
1380
        );
1381
    }
1382

1383
    #[test]
1384
    fn test_load_tile_data() {
3✔
1385
        let output_shape: GridShape2D = [8, 8].into();
1✔
1386
        let output_bounds =
1✔
1387
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1388

1389
        let RasterTile2D {
1✔
1390
            global_geo_transform: _,
1391
            grid_array: grid,
1✔
1392
            tile_position: _,
1393
            time: _,
1394
            properties,
1✔
1395
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1396

1397
        assert!(!grid.is_empty());
2✔
1398

1399
        let grid = grid.into_materialized_masked_grid();
1✔
1400

1401
        assert_eq!(grid.inner_grid.data.len(), 64);
2✔
1402
        assert_eq!(
2✔
1403
            grid.inner_grid.data,
1404
            &[
1405
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
1406
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
1407
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
1408
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
1409
            ]
1410
        );
1411

1412
        assert_eq!(grid.validity_mask.data.len(), 64);
2✔
1413
        assert_eq!(grid.validity_mask.data, &[true; 64]);
2✔
1414

1415
        assert!(properties.scale.is_none());
2✔
1416
        assert!(properties.offset.is_none());
2✔
1417
        assert_eq!(
2✔
1418
            properties.properties_map.get(&RasterPropertiesKey {
3✔
1419
                key: "AREA_OR_POINT".to_string(),
1✔
1420
                domain: None,
1✔
1421
            }),
1422
            Some(&RasterPropertiesEntry::String("Area".to_string()))
1✔
1423
        );
1424
        assert_eq!(
1✔
1425
            properties.properties_map.get(&RasterPropertiesKey {
3✔
1426
                domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1✔
1427
                key: "COMPRESSION".to_string(),
1✔
1428
            }),
1429
            Some(&RasterPropertiesEntry::String("LZW".to_string()))
1✔
1430
        );
1431
    }
1432

1433
    #[test]
1434
    fn test_load_tile_data_overlaps_dataset_bounds() {
3✔
1435
        let output_shape: GridShape2D = [8, 8].into();
1✔
1436
        // shift world bbox one pixel up and to the left
1437
        let (x_size, y_size) = (45., 22.5);
1✔
1438
        let output_bounds = SpatialPartition2D::new_unchecked(
1439
            (-180. - x_size, 90. + y_size).into(),
1✔
1440
            (180. - x_size, -90. + y_size).into(),
1✔
1441
        );
1442

1443
        let RasterTile2D {
2✔
1444
            global_geo_transform: _,
1445
            grid_array: grid,
1✔
1446
            tile_position: _,
1447
            time: _,
1448
            properties: _,
1449
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1450

1451
        assert!(!grid.is_empty());
1✔
1452

1453
        let x = grid.into_materialized_masked_grid();
1✔
1454

1455
        assert_eq!(x.inner_grid.data.len(), 64);
2✔
1456
        assert_eq!(
2✔
1457
            x.inner_grid.data,
1458
            &[
1459
                0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 0, 255, 75, 37, 255,
1460
                44, 34, 39, 0, 255, 86, 255, 255, 255, 30, 96, 0, 255, 255, 255, 255, 90, 255, 255,
1461
                0, 255, 255, 202, 255, 193, 255, 255, 0, 255, 255, 89, 255, 111, 255, 255, 0, 255,
1462
                255, 255, 255, 255, 255, 255
1463
            ]
1464
        );
1465
    }
1466

1467
    #[test]
1468
    fn test_load_tile_data_is_inside_single_pixel() {
3✔
1469
        let output_shape: GridShape2D = [8, 8].into();
1✔
1470
        // shift world bbox one pixel up and to the left
1471
        let (x_size, y_size) = (0.000_000_000_01, 0.000_000_000_01);
1✔
1472
        let output_bounds = SpatialPartition2D::new(
1473
            (-116.22222, 66.66666).into(),
1✔
1474
            (-116.22222 + x_size, 66.66666 - y_size).into(),
1✔
1475
        )
1476
        .unwrap();
1477

1478
        let RasterTile2D {
2✔
1479
            global_geo_transform: _,
1480
            grid_array: grid,
1✔
1481
            tile_position: _,
1482
            time: _,
1483
            properties: _,
1484
        } = load_ndvi_jan_2014(output_shape, output_bounds).unwrap();
1485

1486
        assert!(!grid.is_empty());
1✔
1487

1488
        let x = grid.into_materialized_masked_grid();
1✔
1489

1490
        assert_eq!(x.inner_grid.data.len(), 64);
2✔
1491
        assert_eq!(x.inner_grid.data, &[1; 64]);
2✔
1492
    }
1493

1494
    #[tokio::test]
2✔
1495
    async fn test_query_single_time_slice() {
6✔
1496
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1497
        let query_ctx = MockQueryContext::test_default();
1✔
1498
        let id = add_ndvi_dataset(&mut exe_ctx);
2✔
1499

1500
        let output_shape: GridShape2D = [256, 256].into();
1✔
1501
        let output_bounds =
1✔
1502
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1503
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_388_534_400_001); // 2014-01-01
1✔
1504

1505
        let c = query_gdal_source(
1506
            &exe_ctx,
1✔
1507
            &query_ctx,
1✔
1508
            id,
1✔
1509
            output_shape,
1✔
1510
            output_bounds,
1✔
1511
            time_interval,
1✔
1512
        )
1513
        .await;
4✔
1514
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1515

1516
        assert_eq!(c.len(), 4);
2✔
1517

1518
        assert_eq!(
1✔
1519
            c[0].time,
1✔
1520
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1521
        );
1522

1523
        assert_eq!(
1✔
1524
            c[0].tile_information().global_tile_position(),
2✔
1525
            [-1, -1].into()
1✔
1526
        );
1527

1528
        assert_eq!(
1✔
1529
            c[1].tile_information().global_tile_position(),
2✔
1530
            [-1, 0].into()
1✔
1531
        );
1532

1533
        assert_eq!(
1✔
1534
            c[2].tile_information().global_tile_position(),
2✔
1535
            [0, -1].into()
1✔
1536
        );
1537

1538
        assert_eq!(
3✔
1539
            c[3].tile_information().global_tile_position(),
2✔
1540
            [0, 0].into()
1✔
1541
        );
1542
    }
1543

1544
    #[tokio::test]
2✔
1545
    async fn test_query_multi_time_slices() {
6✔
1546
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1547
        let query_ctx = MockQueryContext::test_default();
1✔
1548
        let id = add_ndvi_dataset(&mut exe_ctx);
2✔
1549

1550
        let output_shape: GridShape2D = [256, 256].into();
1✔
1551
        let output_bounds =
1✔
1552
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1553
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_393_632_000_000); // 2014-01-01 - 2014-03-01
1✔
1554

1555
        let c = query_gdal_source(
1556
            &exe_ctx,
1✔
1557
            &query_ctx,
1✔
1558
            id,
1✔
1559
            output_shape,
1✔
1560
            output_bounds,
1✔
1561
            time_interval,
1✔
1562
        )
1563
        .await;
4✔
1564
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1565

1566
        assert_eq!(c.len(), 8);
2✔
1567

1568
        assert_eq!(
1✔
1569
            c[0].time,
1✔
1570
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1571
        );
1572

1573
        assert_eq!(
3✔
1574
            c[5].time,
1✔
1575
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1576
        );
1577
    }
1578

1579
    #[tokio::test]
2✔
1580
    async fn test_query_before_data() {
6✔
1581
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1582
        let query_ctx = MockQueryContext::test_default();
1✔
1583
        let id = add_ndvi_dataset(&mut exe_ctx);
2✔
1584

1585
        let output_shape: GridShape2D = [256, 256].into();
1✔
1586
        let output_bounds =
1✔
1587
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1588
        let time_interval = TimeInterval::new_unchecked(1_380_585_600_000, 1_380_585_600_000); // 2013-10-01 - 2013-10-01
1✔
1589

1590
        let c = query_gdal_source(
1591
            &exe_ctx,
1✔
1592
            &query_ctx,
1✔
1593
            id,
1✔
1594
            output_shape,
1✔
1595
            output_bounds,
1✔
1596
            time_interval,
1✔
1597
        )
1598
        .await;
3✔
1599
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1600

1601
        assert_eq!(c.len(), 4);
2✔
1602

1603
        assert_eq!(
3✔
1604
            c[0].time,
1✔
1605
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000) // bot - 2014-01-01
1✔
1606
        );
1607
    }
1608

1609
    #[tokio::test]
2✔
1610
    async fn test_query_after_data() {
6✔
1611
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1612
        let query_ctx = MockQueryContext::test_default();
1✔
1613
        let id = add_ndvi_dataset(&mut exe_ctx);
2✔
1614

1615
        let output_shape: GridShape2D = [256, 256].into();
1✔
1616
        let output_bounds =
1✔
1617
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1618
        let time_interval = TimeInterval::new_unchecked(1_420_074_000_000, 1_420_074_000_000); // 2015-01-01 - 2015-01-01
1✔
1619

1620
        let c = query_gdal_source(
1621
            &exe_ctx,
1✔
1622
            &query_ctx,
1✔
1623
            id,
1✔
1624
            output_shape,
1✔
1625
            output_bounds,
1✔
1626
            time_interval,
1✔
1627
        )
1628
        .await;
3✔
1629
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1630

1631
        assert_eq!(c.len(), 4);
2✔
1632

1633
        assert_eq!(
3✔
1634
            c[0].time,
1✔
1635
            TimeInterval::new_unchecked(1_404_172_800_000, TimeInstance::MAX) // 2014-07-01 - eot
1✔
1636
        );
1637
    }
1638

1639
    #[tokio::test]
2✔
1640
    async fn test_nodata() {
6✔
1641
        hide_gdal_errors();
1✔
1642

1643
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1644
        let query_ctx = MockQueryContext::test_default();
1✔
1645
        let id = add_ndvi_dataset(&mut exe_ctx);
2✔
1646

1647
        let output_shape: GridShape2D = [256, 256].into();
1✔
1648
        let output_bounds =
1✔
1649
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1650
        let time_interval = TimeInterval::new_unchecked(1_385_856_000_000, 1_388_534_400_000); // 2013-12-01 - 2014-01-01
1✔
1651

1652
        let c = query_gdal_source(
1653
            &exe_ctx,
1✔
1654
            &query_ctx,
1✔
1655
            id,
1✔
1656
            output_shape,
1✔
1657
            output_bounds,
1✔
1658
            time_interval,
1✔
1659
        )
1660
        .await;
3✔
1661
        let c: Vec<RasterTile2D<u8>> = c.into_iter().map(Result::unwrap).collect();
1✔
1662

1663
        assert_eq!(c.len(), 4);
2✔
1664

1665
        let tile_1 = &c[0];
2✔
1666

1667
        assert_eq!(
1✔
1668
            tile_1.time,
1669
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1670
        );
1671

1672
        assert!(tile_1.is_empty());
4✔
1673
    }
1674

1675
    #[tokio::test]
2✔
1676
    async fn timestep_without_params() {
6✔
1677
        let output_bounds =
1✔
1678
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1679
        let output_shape: GridShape2D = [256, 256].into();
1✔
1680

1681
        let tile_info = TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
1682
        let time_interval = TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000); // 2014-01-01 - 2014-01-15
1✔
1683
        let params = None;
1✔
1684

1685
        let tile = GdalRasterLoader::load_tile_async::<f64>(params, tile_info, time_interval).await;
2✔
1686

1687
        assert!(tile.is_ok());
2✔
1688

1689
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1690
            time_interval,
1✔
1691
            tile_info,
1✔
1692
            EmptyGrid2D::new(output_shape).into(),
2✔
1693
        );
1694

1695
        assert_eq!(tile.unwrap(), expected);
4✔
1696
    }
1697

1698
    #[test]
1699
    fn it_reverts_config_options() {
3✔
1700
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1701

1702
        {
1703
            let _config =
2✔
1704
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1705

1706
            assert_eq!(
×
1707
                gdal::config::get_config_option("foo", "default").unwrap(),
2✔
1708
                "bar".to_owned()
1✔
1709
            );
1710
        }
1711

1712
        assert_eq!(
×
1713
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1714
            String::new()
1✔
1715
        );
1716
    }
1717

1718
    #[test]
1719
    #[allow(clippy::too_many_lines)]
1720
    fn deserialize_dataset_parameters() {
3✔
1721
        let dataset_parameters = GdalDatasetParameters {
1722
            file_path: "path-to-data.tiff".into(),
1✔
1723
            rasterband_channel: 1,
1724
            geo_transform: GdalDatasetGeoTransform {
1✔
1725
                origin_coordinate: (-180., 90.).into(),
1726
                x_pixel_size: 0.1,
1727
                y_pixel_size: -0.1,
1728
            },
1729
            width: 3600,
1730
            height: 1800,
1731
            file_not_found_handling: FileNotFoundHandling::NoData,
1732
            no_data_value: Some(f64::NAN),
1✔
1733
            properties_mapping: Some(vec![
2✔
1734
                GdalMetadataMapping {
1735
                    source_key: RasterPropertiesKey {
1736
                        domain: None,
1737
                        key: "AREA_OR_POINT".to_string(),
1738
                    },
1739
                    target_type: RasterPropertiesEntryType::String,
1740
                    target_key: RasterPropertiesKey {
1741
                        domain: None,
1742
                        key: "AREA_OR_POINT".to_string(),
1743
                    },
1744
                },
1745
                GdalMetadataMapping {
1746
                    source_key: RasterPropertiesKey {
1747
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1748
                        key: "COMPRESSION".to_string(),
1749
                    },
1750
                    target_type: RasterPropertiesEntryType::String,
1751
                    target_key: RasterPropertiesKey {
1752
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
1753
                        key: "COMPRESSION".to_string(),
1754
                    },
1755
                },
1756
            ]),
1757
            gdal_open_options: None,
1758
            gdal_config_options: None,
1759
            allow_alphaband_as_mask: true,
1760
        };
1761

1762
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
2✔
1763

1764
        assert_eq!(
1✔
1765
            dataset_parameters_json,
1766
            serde_json::json!({
2✔
1767
                "filePath": "path-to-data.tiff",
1768
                "rasterbandChannel": 1,
1769
                "geoTransform": {
1770
                    "originCoordinate": {
1771
                        "x": -180.,
1772
                        "y": 90.
1773
                    },
1774
                    "xPixelSize": 0.1,
1775
                    "yPixelSize": -0.1
1776
                },
1777
                "width": 3600,
1778
                "height": 1800,
1779
                "fileNotFoundHandling": "NoData",
1780
                "noDataValue": "nan",
1781
                "propertiesMapping": [{
1782
                        "source_key": {
1783
                            "domain": null,
1784
                            "key": "AREA_OR_POINT"
1785
                        },
1786
                        "target_key": {
1787
                            "domain": null,
1788
                            "key": "AREA_OR_POINT"
1789
                        },
1790
                        "target_type": "String"
1791
                    },
1792
                    {
1793
                        "source_key": {
1794
                            "domain": "IMAGE_STRUCTURE",
1795
                            "key": "COMPRESSION"
1796
                        },
1797
                        "target_key": {
1798
                            "domain": "IMAGE_STRUCTURE_INFO",
1799
                            "key": "COMPRESSION"
1800
                        },
1801
                        "target_type": "String"
1802
                    }
1803
                ],
1804
                "gdalOpenOptions": null,
1805
                "gdalConfigOptions": null,
1806
                "allowAlphabandAsMask": true,
1807
            })
1808
        );
1809

1810
        let deserialized_parameters =
1✔
1811
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1812

1813
        // since there is NaN in the data, we can't check for equality on the whole object
1814

1815
        assert_eq!(
2✔
1816
            deserialized_parameters.file_path,
1817
            dataset_parameters.file_path,
1818
        );
1819
        assert_eq!(
1✔
1820
            deserialized_parameters.rasterband_channel,
1821
            dataset_parameters.rasterband_channel,
1822
        );
1823
        assert_eq!(
2✔
1824
            deserialized_parameters.geo_transform,
1825
            dataset_parameters.geo_transform,
1826
        );
1827
        assert_eq!(deserialized_parameters.width, dataset_parameters.width);
1✔
1828
        assert_eq!(deserialized_parameters.height, dataset_parameters.height);
1✔
1829
        assert_eq!(
2✔
1830
            deserialized_parameters.file_not_found_handling,
1831
            dataset_parameters.file_not_found_handling,
1832
        );
1833
        assert!(
1✔
1834
            deserialized_parameters.no_data_value.unwrap().is_nan()
3✔
1835
                && dataset_parameters.no_data_value.unwrap().is_nan()
1✔
1836
        );
1837
        assert_eq!(
2✔
1838
            deserialized_parameters.properties_mapping,
1839
            dataset_parameters.properties_mapping,
1840
        );
1841
        assert_eq!(
2✔
1842
            deserialized_parameters.gdal_open_options,
1843
            dataset_parameters.gdal_open_options,
1844
        );
1845
        assert_eq!(
2✔
1846
            deserialized_parameters.gdal_config_options,
1847
            dataset_parameters.gdal_config_options,
1848
        );
1849
    }
1850

1851
    #[test]
1852
    fn gdal_geotransform_to_bounds_neg_y_0() {
3✔
1853
        let gt = GdalDatasetGeoTransform {
1854
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1855
            x_pixel_size: 1.,
1856
            y_pixel_size: -1.,
1857
        };
1858

1859
        let sb = gt.spatial_partition(10, 10);
1✔
1860

1861
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
1862
            .unwrap();
1863

1864
        assert_eq!(sb, exp);
1✔
1865
    }
1866

1867
    #[test]
1868
    fn gdal_geotransform_to_bounds_neg_y_5() {
3✔
1869
        let gt = GdalDatasetGeoTransform {
1870
            origin_coordinate: Coordinate2D::new(5., 5.),
1✔
1871
            x_pixel_size: 0.5,
1872
            y_pixel_size: -0.5,
1873
        };
1874

1875
        let sb = gt.spatial_partition(10, 10);
1✔
1876

1877
        let exp =
1✔
1878
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1879

1880
        assert_eq!(sb, exp);
1✔
1881
    }
1882

1883
    #[test]
1884
    fn gdal_geotransform_to_bounds_pos_y_0() {
3✔
1885
        let gt = GdalDatasetGeoTransform {
1886
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1887
            x_pixel_size: 1.,
1888
            y_pixel_size: 1.,
1889
        };
1890

1891
        let sb = gt.spatial_partition(10, 10);
1✔
1892

1893
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
1894
            .unwrap();
1895

1896
        assert_eq!(sb, exp);
1✔
1897
    }
1898

1899
    #[test]
1900
    fn gdal_geotransform_to_bounds_pos_y_5() {
3✔
1901
        let gt = GdalDatasetGeoTransform {
1902
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
1903
            x_pixel_size: 0.5,
1904
            y_pixel_size: 0.5,
1905
        };
1906

1907
        let sb = gt.spatial_partition(10, 10);
1✔
1908

1909
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
1910
            .unwrap();
1911

1912
        assert_eq!(sb, exp);
1✔
1913
    }
1914

1915
    #[test]
1916
    fn gdal_read_window_data_origin_upper_left() {
3✔
1917
        let gt = GdalDatasetGeoTransform {
1918
            origin_coordinate: Coordinate2D::new(5., -5.),
1✔
1919
            x_pixel_size: 0.5,
1920
            y_pixel_size: -0.5,
1921
        };
1922

1923
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
1924
            .unwrap();
1925

1926
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
1927

1928
        let exp = GdalReadWindow {
1929
            read_size_x: 4,
1930
            read_size_y: 6,
1931
            read_start_x: 6,
1932
            read_start_y: 4,
1933
        };
1934

1935
        assert_eq!(rw, exp);
1✔
1936
    }
1937

1938
    #[test]
1939
    fn gdal_read_window_data_origin_lower_left() {
3✔
1940
        let gt = GdalDatasetGeoTransform {
1941
            origin_coordinate: Coordinate2D::new(0., 0.),
1✔
1942
            x_pixel_size: 1.,
1943
            y_pixel_size: 1.,
1944
        };
1945

1946
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
1947
            .unwrap();
1948

1949
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
1950

1951
        let exp = GdalReadWindow {
1952
            read_size_x: 10,
1953
            read_size_y: 10,
1954
            read_start_x: 0,
1955
            read_start_y: 0,
1956
        };
1957

1958
        assert_eq!(rw, exp);
1✔
1959
    }
1960

1961
    #[test]
1962
    fn read_up_side_down_raster() {
3✔
1963
        let output_shape: GridShape2D = [8, 8].into();
1✔
1964
        let output_bounds =
1✔
1965
            SpatialPartition2D::new_unchecked((-180., 90.).into(), (180., -90.).into());
1966

1967
        let up_side_down_params = GdalDatasetParameters {
1968
            file_path: test_data!(
4✔
1969
                "raster/modis_ndvi/flipped_axis_y/MOD13A2_M_NDVI_2014-01-01_flipped_y.tiff"
1970
            )
1971
            .into(),
1972
            rasterband_channel: 1,
1973
            geo_transform: GdalDatasetGeoTransform {
1✔
1974
                origin_coordinate: (-180., -90.).into(),
1975
                x_pixel_size: 0.1,
1976
                y_pixel_size: 0.1,
1977
            },
1978
            width: 3600,
1979
            height: 1800,
1980
            file_not_found_handling: FileNotFoundHandling::NoData,
1981
            no_data_value: Some(0.),
1✔
1982
            properties_mapping: Some(vec![
2✔
1983
                GdalMetadataMapping {
1984
                    source_key: RasterPropertiesKey {
1985
                        domain: None,
1986
                        key: "AREA_OR_POINT".to_string(),
1987
                    },
1988
                    target_type: RasterPropertiesEntryType::String,
1989
                    target_key: RasterPropertiesKey {
1990
                        domain: None,
1991
                        key: "AREA_OR_POINT".to_string(),
1992
                    },
1993
                },
1994
                GdalMetadataMapping {
1995
                    source_key: RasterPropertiesKey {
1996
                        domain: Some("IMAGE_STRUCTURE".to_string()),
1997
                        key: "COMPRESSION".to_string(),
1998
                    },
1999
                    target_type: RasterPropertiesEntryType::String,
2000
                    target_key: RasterPropertiesKey {
2001
                        domain: Some("IMAGE_STRUCTURE_INFO".to_string()),
2002
                        key: "COMPRESSION".to_string(),
2003
                    },
2004
                },
2005
            ]),
2006
            gdal_open_options: None,
2007
            gdal_config_options: None,
2008
            allow_alphaband_as_mask: true,
2009
        };
2010

2011
        let tile_information =
1✔
2012
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
2013

2014
        let RasterTile2D {
2015
            global_geo_transform: _,
2016
            grid_array: grid,
1✔
2017
            tile_position: _,
2018
            time: _,
2019
            properties: _,
2020
        } = GdalRasterLoader::load_tile_data::<u8>(
2021
            &up_side_down_params,
2022
            tile_information,
1✔
2023
            TimeInterval::default(),
1✔
2024
        )
2025
        .unwrap();
2026

2027
        assert!(!grid.is_empty());
1✔
2028

2029
        let grid = grid.into_materialized_masked_grid();
1✔
2030

2031
        assert_eq!(grid.inner_grid.data.len(), 64);
2✔
2032
        assert_eq!(
2✔
2033
            grid.inner_grid.data,
2034
            &[
2035
                255, 255, 255, 255, 255, 255, 255, 255, 255, 75, 37, 255, 44, 34, 39, 32, 255, 86,
2036
                255, 255, 255, 30, 96, 255, 255, 255, 255, 255, 90, 255, 255, 255, 255, 255, 202,
2037
                255, 193, 255, 255, 255, 255, 255, 89, 255, 111, 255, 255, 255, 255, 255, 255, 255,
2038
                255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
2039
            ]
2040
        );
2041

2042
        assert_eq!(grid.validity_mask.data.len(), 64);
2✔
2043
        assert_eq!(grid.validity_mask.data, &[true; 64]);
2✔
2044
    }
2045
}
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