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

geo-engine / geoengine / 4056270646

pending completion
4056270646

push

github

GitHub
Merge #706

108 of 108 new or added lines in 5 files covered. (100.0%)

87598 of 99672 relevant lines covered (87.89%)

77046.89 hits per line

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

92.27
/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)]
67✔
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>) {
2✔
95
        data_ids.push(self.data.clone());
2✔
96
    }
2✔
97
}
98

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

102
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
203✔
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)]
203✔
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)]
976✔
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)]
2✔
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) {
531✔
150
        (self.read_start_x, self.read_start_y)
531✔
151
    }
531✔
152

153
    fn gdal_window_size(&self) -> (usize, usize) {
531✔
154
        (self.read_size_x, self.read_size_y)
531✔
155
    }
531✔
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)]
976✔
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 {
1,100✔
174
        // the opposite y value (y value of the non origin edge)
1,100✔
175
        let opposite_coord_y = self.origin_coordinate.y + self.y_pixel_size * y_size as f64;
1,100✔
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() {
1,100✔
179
            (self.origin_coordinate.y, opposite_coord_y)
1,097✔
180
        } else {
181
            (opposite_coord_y, self.origin_coordinate.y)
3✔
182
        };
183

184
        let opposite_coord_x = self.origin_coordinate.x + self.x_pixel_size * x_size as f64;
1,100✔
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() {
1,100✔
188
            (self.origin_coordinate.x, opposite_coord_x)
1,100✔
189
        } else {
190
            (opposite_coord_x, self.origin_coordinate.x)
×
191
        };
192

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

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

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

211
    fn spatial_partition_to_read_window(
525✔
212
        &self,
525✔
213
        spatial_partition: &SpatialPartition2D,
525✔
214
    ) -> GdalReadWindow {
525✔
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`].
525✔
216
        const EPSILON: f64 = 0.000_001;
525✔
217
        let epsilon: Coordinate2D =
525✔
218
            (self.x_pixel_size * EPSILON, self.y_pixel_size * EPSILON).into();
525✔
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() {
525✔
251
            (
523✔
252
                spatial_partition.upper_left(),
523✔
253
                spatial_partition.lower_right(),
523✔
254
            )
523✔
255
        } else {
256
            (
2✔
257
                spatial_partition.lower_left(),
2✔
258
                spatial_partition.upper_right(),
2✔
259
            )
2✔
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;
525✔
264
        // Move the coordinate far from the origin a bit inside the bbox by subtracting an epsilon of the pixel size
525✔
265
        let safe_far_coord = far_origin_coord - epsilon;
525✔
266

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

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

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

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

285
/// Default implementation for testing purposes where geo transform doesn't matter
286
impl TestDefault for GdalDatasetGeoTransform {
287
    fn test_default() -> Self {
11✔
288
        Self {
11✔
289
            origin_coordinate: (0.0, 0.0).into(),
11✔
290
            x_pixel_size: 1.0,
11✔
291
            y_pixel_size: -1.0,
11✔
292
        }
11✔
293
    }
11✔
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
523✔
300
    where
523✔
301
        M: Into<Self::Margin>,
523✔
302
    {
523✔
303
        let m = margin.into();
523✔
304
        self.origin_coordinate.approx_eq(other.origin_coordinate, m)
523✔
305
            && self.x_pixel_size.approx_eq(other.x_pixel_size, m)
523✔
306
            && self.y_pixel_size.approx_eq(other.y_pixel_size, m)
523✔
307
    }
523✔
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 {
578✔
330
        Self {
578✔
331
            origin_coordinate: (gdal_geo_transform[0], gdal_geo_transform[3]).into(),
578✔
332
            x_pixel_size: gdal_geo_transform[1],
578✔
333
            y_pixel_size: gdal_geo_transform[5],
578✔
334
        }
578✔
335
    }
578✔
336
}
337

338
impl SpatialPartitioned for GdalDatasetParameters {
339
    fn spatial_partition(&self) -> SpatialPartition2D {
573✔
340
        self.geo_transform
573✔
341
            .spatial_partition(self.width, self.height)
573✔
342
    }
573✔
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)]
976✔
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(
107✔
364
        &self,
107✔
365
        placeholders: &HashMap<String, GdalSourceTimePlaceholder>,
107✔
366
        time: TimeInterval,
107✔
367
    ) -> Result<Self> {
107✔
368
        let mut file_path: String = self.file_path.to_string_lossy().into();
107✔
369

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

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

384
        Ok(Self {
107✔
385
            file_not_found_handling: self.file_not_found_handling,
107✔
386
            file_path: file_path.into(),
107✔
387
            properties_mapping: self.properties_mapping.clone(),
107✔
388
            gdal_open_options: self.gdal_open_options.clone(),
107✔
389
            gdal_config_options: self.gdal_config_options.clone(),
107✔
390
            ..*self
107✔
391
        })
107✔
392
    }
107✔
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>(
519✔
419
        dataset_params: GdalDatasetParameters,
519✔
420
        tile_information: TileInformation,
519✔
421
        tile_time: TimeInterval,
519✔
422
    ) -> Result<RasterTile2D<T>> {
519✔
423
        crate::util::spawn_blocking(move || {
519✔
424
            Self::load_tile_data(&dataset_params, tile_information, tile_time)
519✔
425
        })
519✔
426
        .await
494✔
427
        .context(crate::error::TokioJoin)?
506✔
428
    }
506✔
429

430
    async fn load_tile_async<T: Pixel + GdalType + FromPrimitive>(
637✔
431
        dataset_params: Option<GdalDatasetParameters>,
637✔
432
        tile_information: TileInformation,
637✔
433
        tile_time: TimeInterval,
637✔
434
    ) -> Result<RasterTile2D<T>> {
637✔
435
        match dataset_params {
573✔
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)
519✔
438
                if tile_information
573✔
439
                    .spatial_partition()
573✔
440
                    .intersects(&ds.spatial_partition()) =>
573✔
441
            {
442
                debug!(
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
519✔
447
            }
448
            Some(_) => {
449
                debug!("Skipping tile not in query rect {:?}", &tile_information);
×
450

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

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

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

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

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

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

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

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

523✔
501
        if dataset_result.is_err() {
523✔
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
        };
523✔
521

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

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

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

534
        Ok(result_tile)
523✔
535
    }
523✔
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>(
118✔
541
        query: RasterQueryRectangle,
118✔
542
        info: GdalLoadingInfoTemporalSlice,
118✔
543
        tiling_strategy: TilingStrategy,
118✔
544
    ) -> impl Stream<Item = impl Future<Output = Result<RasterTile2D<T>>>> {
118✔
545
        stream::iter(tiling_strategy.tile_information_iterator(query.spatial_bounds)).map(
118✔
546
            move |tile| GdalRasterLoader::load_tile_async(info.params.clone(), tile, info.time),
636✔
547
        )
118✔
548
    }
118✔
549

550
    fn loading_info_to_tile_stream<
106✔
551
        T: Pixel + GdalType + FromPrimitive,
106✔
552
        S: Stream<Item = Result<GdalLoadingInfoTemporalSlice>>,
106✔
553
    >(
106✔
554
        loading_info_stream: S,
106✔
555
        query: RasterQueryRectangle,
106✔
556
        tiling_strategy: TilingStrategy,
106✔
557
    ) -> impl Stream<Item = Result<RasterTile2D<T>>> {
106✔
558
        loading_info_stream
106✔
559
            .map_ok(move |info| {
118✔
560
                GdalRasterLoader::temporal_slice_tile_future_stream(query, info, tiling_strategy)
118✔
561
                    .map(Result::Ok)
118✔
562
            })
118✔
563
            .try_flatten()
106✔
564
            .try_buffered(16) // TODO: make this configurable
106✔
565
    }
106✔
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>(
106✔
579
        &'a self,
106✔
580
        query: RasterQueryRectangle,
106✔
581
        _ctx: &'a dyn crate::engine::QueryContext,
106✔
582
    ) -> Result<BoxStream<Result<Self::Output>>> {
106✔
583
        let start = Instant::now();
106✔
584
        debug!(
106✔
585
            "Querying GdalSourceProcessor<{:?}> with: {:?}.",
×
586
            P::TYPE,
×
587
            &query
×
588
        );
589

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

596
        let spatial_resolution = query.spatial_resolution;
106✔
597

106✔
598
        // A `GeoTransform` maps pixel space to world space.
106✔
599
        // Usually a SRS has axis directions pointing "up" (y-axis) and "up" (y-axis).
106✔
600
        // We are not aware of spatial reference systems where the x-axis points to the right.
106✔
601
        // However, there are spatial reference systems where the y-axis points downwards.
106✔
602
        // The standard "pixel-space" starts at the top-left corner of a `GeoTransform` and points down-right.
106✔
603
        // Therefore, the pixel size on the x-axis is always increasing
106✔
604
        let pixel_size_x = spatial_resolution.x;
106✔
605
        debug_assert!(pixel_size_x.is_sign_positive());
106✔
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;
106✔
609
        debug_assert!(pixel_size_y.is_sign_negative());
106✔
610

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

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

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

621
        if let Some(data_spatial_bounds) = result_descriptor.bbox {
106✔
622
            if !data_spatial_bounds.intersects(&query.spatial_bounds) {
66✔
623
                debug!("query does not intersect spatial data bounds");
×
624
                empty = true;
×
625
            }
66✔
626
        }
40✔
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 {
106✔
639
            GdalLoadingInfoTemporalSliceIterator::Static {
×
640
                parts: vec![].into_iter(),
×
641
            }
×
642
        } else {
643
            let loading_info = self.meta_data.loading_info(query).await?;
106✔
644
            loading_info.info
106✔
645
        };
646

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

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

106✔
652
        // use SparseTilesFillAdapter to fill all the gaps
106✔
653
        let filled_stream = SparseTilesFillAdapter::new(
106✔
654
            source_stream,
106✔
655
            tiling_strategy.tile_grid_box(query.spatial_partition()),
106✔
656
            tiling_strategy.geo_transform,
106✔
657
            tiling_strategy.tile_size_in_pixels,
106✔
658
        );
106✔
659
        Ok(filled_stream.boxed())
106✔
660
    }
212✔
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]
10✔
670
#[async_trait]
671
impl RasterOperator for GdalSource {
672
    async fn _initialize(
42✔
673
        self: Box<Self>,
42✔
674
        context: &dyn crate::engine::ExecutionContext,
42✔
675
    ) -> Result<Box<dyn InitializedRasterOperator>> {
42✔
676
        let meta_data: GdalMetaData = context.meta_data(&self.params.data).await?;
42✔
677

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

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

42✔
686
        Ok(op.boxed())
42✔
687
    }
84✔
688

689
    span_fn!(GdalSource);
1✔
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 {
73✔
700
        &self.result_descriptor
73✔
701
    }
73✔
702

703
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
36✔
704
        Ok(match self.result_descriptor().data_type {
36✔
705
            RasterDataType::U8 => TypedRasterQueryProcessor::U8(
33✔
706
                GdalSourceProcessor {
33✔
707
                    tiling_specification: self.tiling_specification,
33✔
708
                    meta_data: self.meta_data.clone(),
33✔
709
                    _phantom_data: PhantomData,
33✔
710
                }
33✔
711
                .boxed(),
33✔
712
            ),
33✔
713
            RasterDataType::U16 => TypedRasterQueryProcessor::U16(
2✔
714
                GdalSourceProcessor {
2✔
715
                    tiling_specification: self.tiling_specification,
2✔
716
                    meta_data: self.meta_data.clone(),
2✔
717
                    _phantom_data: PhantomData,
2✔
718
                }
2✔
719
                .boxed(),
2✔
720
            ),
2✔
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(
1✔
740
                GdalSourceProcessor {
1✔
741
                    tiling_specification: self.tiling_specification,
1✔
742
                    meta_data: self.meta_data.clone(),
1✔
743
                    _phantom_data: PhantomData,
1✔
744
                }
1✔
745
                .boxed(),
1✔
746
            ),
1✔
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
    }
36✔
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>(
523✔
784
    rasterband: &GdalRasterBand,
523✔
785
    read_window: &GdalReadWindow,
523✔
786
    out_shape: D,
523✔
787
    dataset_params: &GdalDatasetParameters,
523✔
788
    flip_y_axis: bool,
523✔
789
) -> Result<GridOrEmpty<D, T>>
523✔
790
where
523✔
791
    T: Pixel + GdalType + Default + FromPrimitive,
523✔
792
    D: Clone + GridSize + PartialEq,
523✔
793
{
523✔
794
    let gdal_out_shape = (out_shape.axis_size_x(), out_shape.axis_size_y());
523✔
795

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

804
    let data_grid = if flip_y_axis {
523✔
805
        data_grid.reversed_y_axis_grid()
1✔
806
    } else {
807
        data_grid
522✔
808
    };
809

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

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

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

8✔
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
    }
8✔
834

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

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

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

852
    let masked_grid = MaskedGrid::new(data_grid, mask_grid)?;
8✔
853
    Ok(GridOrEmpty::from(masked_grid))
8✔
854
}
523✔
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>(
286✔
860
    rasterband: &GdalRasterBand,
286✔
861
    gdal_read_window: &GdalReadWindow,
286✔
862
    out_tile_read_bounds: GridBoundingBox2D,
286✔
863
    out_tile_shape: GridShape2D,
286✔
864
    dataset_params: &GdalDatasetParameters,
286✔
865
    flip_y_axis: bool,
286✔
866
) -> Result<GridOrEmpty2D<T>>
286✔
867
where
286✔
868
    T: Pixel + GdalType + Default + FromPrimitive,
286✔
869
{
286✔
870
    let dataset_raster = read_grid_from_raster(
286✔
871
        rasterband,
286✔
872
        gdal_read_window,
286✔
873
        out_tile_read_bounds,
286✔
874
        dataset_params,
286✔
875
        flip_y_axis,
286✔
876
    )?;
286✔
877

878
    let mut tile_raster = GridOrEmpty::from(EmptyGrid::new(out_tile_shape));
286✔
879
    tile_raster.grid_blit_from(&dataset_raster);
286✔
880
    Ok(tile_raster)
286✔
881
}
286✔
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>(
523✔
888
    tile_info: TileInformation,
523✔
889
    dataset: &GdalDataset,
523✔
890
    rasterband: &GdalRasterBand,
523✔
891
    dataset_params: &GdalDatasetParameters,
523✔
892
) -> Result<GridOrEmpty2D<T>>
523✔
893
where
523✔
894
    T: Pixel + GdalType + Default + FromPrimitive,
523✔
895
{
523✔
896
    let gdal_dataset_geotransform = GdalDatasetGeoTransform::from(dataset.geo_transform()?);
523✔
897
    let (gdal_dataset_pixels_x, gdal_dataset_pixels_y) = dataset.raster_size();
523✔
898

523✔
899
    if !approx_eq!(
523✔
900
        GdalDatasetGeoTransform,
523✔
901
        gdal_dataset_geotransform,
523✔
902
        dataset_params.geo_transform
523✔
903
    ) {
523✔
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
    };
523✔
910

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

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

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

921
    let Some(dataset_intersection_area) = dataset_intersects_tile else {
523✔
922
        return Ok(GridOrEmpty::from(EmptyGrid::new(output_shape)));
×
923
    };
924

925
    let tile_geo_transform = tile_info.tile_geo_transform();
523✔
926

523✔
927
    let gdal_read_window =
523✔
928
        gdal_dataset_geotransform.spatial_partition_to_read_window(&dataset_intersection_area);
523✔
929

523✔
930
    let is_y_axis_flipped = tile_geo_transform.y_pixel_size().is_sign_negative()
523✔
931
        != gdal_dataset_geotransform.y_pixel_size.is_sign_negative();
523✔
932

523✔
933
    if is_y_axis_flipped {
523✔
934
        debug!("The GDAL data has a flipped y-axis. Need to unflip it!");
1✔
935
    }
522✔
936

937
    let result_grid = if dataset_intersection_area == output_bounds {
523✔
938
        read_grid_from_raster(
237✔
939
            rasterband,
237✔
940
            &gdal_read_window,
237✔
941
            output_shape,
237✔
942
            dataset_params,
237✔
943
            is_y_axis_flipped,
237✔
944
        )?
237✔
945
    } else {
946
        let partial_tile_grid_bounds =
286✔
947
            tile_geo_transform.spatial_to_grid_bounds(&dataset_intersection_area);
286✔
948

286✔
949
        read_partial_grid_from_raster(
286✔
950
            rasterband,
286✔
951
            &gdal_read_window,
286✔
952
            partial_tile_grid_bounds,
286✔
953
            output_shape,
286✔
954
            dataset_params,
286✔
955
            is_y_axis_flipped,
286✔
956
        )?
286✔
957
    };
958

959
    Ok(result_grid)
523✔
960
}
523✔
961

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

971
    let result_grid = read_grid_and_handle_edges(tile_info, dataset, &rasterband, dataset_params)?;
523✔
972

973
    let mut properties = RasterProperties::default();
523✔
974

975
    if let Some(properties_mapping) = dataset_params.properties_mapping.as_ref() {
523✔
976
        properties_from_gdal(&mut properties, dataset, properties_mapping);
4✔
977
        let rasterband = dataset.rasterband(dataset_params.rasterband_channel as isize)?;
4✔
978
        properties_from_gdal(&mut properties, &rasterband, properties_mapping);
4✔
979
        properties_from_band(&mut properties, &rasterband);
4✔
980
    }
519✔
981

982
    Ok(RasterTile2D::new_with_tile_info_and_properties(
523✔
983
        tile_time,
523✔
984
        tile_info,
523✔
985
        result_grid,
523✔
986
        properties,
523✔
987
    ))
523✔
988
}
523✔
989

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

1002
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14✔
1003
pub struct GdalMetadataMapping {
1004
    pub source_key: RasterPropertiesKey,
1005
    pub target_key: RasterPropertiesKey,
1006
    pub target_type: RasterPropertiesEntryType,
1007
}
1008

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

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

1032
    for m in mapping_iter {
24✔
1033
        let data = if let Some(domain) = &m.source_key.domain {
16✔
1034
            gdal_dataset.metadata_item(&m.source_key.key, domain)
8✔
1035
        } else {
1036
            gdal_dataset.metadata_item(&m.source_key.key, "")
8✔
1037
        };
1038

1039
        if let Some(d) = data {
16✔
1040
            let entry = match m.target_type {
8✔
1041
                RasterPropertiesEntryType::Number => d.parse::<f64>().map_or_else(
×
1042
                    |_| RasterPropertiesEntry::String(d),
×
1043
                    RasterPropertiesEntry::Number,
×
1044
                ),
×
1045
                RasterPropertiesEntryType::String => RasterPropertiesEntry::String(d),
8✔
1046
            };
1047

1048
            debug!(
8✔
1049
                "gdal properties key \"{:?}\" => target key \"{:?}\". Value: {:?} ",
×
1050
                &m.source_key, &m.target_key, &entry
×
1051
            );
1052

1053
            properties
8✔
1054
                .properties_map
8✔
1055
                .insert(m.target_key.clone(), entry);
8✔
1056
        }
8✔
1057
    }
1058
}
8✔
1059

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

1065
    if let Some(offset) = gdal_dataset.metadata_item("offset", "") {
4✔
1066
        properties.offset = offset.parse::<f64>().ok();
×
1067
    };
4✔
1068

1069
    if let Some(band_name) = gdal_dataset.metadata_item("band_name", "") {
4✔
1070
        properties.band_name = Some(band_name);
×
1071
    }
4✔
1072
}
4✔
1073

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

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

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

5✔
1102
        let x_query_resolution = output_bounds.size_x() / output_shape.axis_size_x() as f64;
5✔
1103
        let y_query_resolution = output_bounds.size_y() / output_shape.axis_size_y() as f64;
5✔
1104
        let spatial_resolution =
5✔
1105
            SpatialResolution::new_unchecked(x_query_resolution, y_query_resolution);
5✔
1106

1107
        let o = op.initialize(exe_ctx).await.unwrap();
5✔
1108

5✔
1109
        o.query_processor()
5✔
1110
            .unwrap()
5✔
1111
            .get_u8()
5✔
1112
            .unwrap()
5✔
1113
            .raster_query(
5✔
1114
                RasterQueryRectangle {
5✔
1115
                    spatial_bounds: output_bounds,
5✔
1116
                    time_interval,
5✔
1117
                    spatial_resolution,
5✔
1118
                },
5✔
1119
                query_ctx,
5✔
1120
            )
5✔
1121
            .await
×
1122
            .unwrap()
5✔
1123
            .collect()
5✔
1124
            .await
14✔
1125
    }
5✔
1126

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

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

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

1✔
1191
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1192
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1193
            geo_transform: dataset_geo_transform,
1✔
1194
        };
1✔
1195

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

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

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

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

1✔
1229
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1230
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1231
            geo_transform: central_geo_transform,
1✔
1232
        };
1✔
1233

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

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

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

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

1✔
1267
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1268
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1269
            geo_transform: central_geo_transform,
1✔
1270
        };
1✔
1271

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

1282
    #[test]
1✔
1283
    fn tile_information_iterator() {
1✔
1284
        let tile_size_in_pixels = [600, 600];
1✔
1285
        let dataset_x_pixel_size = 0.1;
1✔
1286
        let dataset_y_pixel_size = -0.1;
1✔
1287

1✔
1288
        let central_geo_transform = GeoTransform::new_with_coordinate_x_y(
1✔
1289
            0.0,
1✔
1290
            dataset_x_pixel_size,
1✔
1291
            0.0,
1✔
1292
            dataset_y_pixel_size,
1✔
1293
        );
1✔
1294

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

1✔
1297
        let origin_split_tileing_strategy = TilingStrategy {
1✔
1298
            tile_size_in_pixels: tile_size_in_pixels.into(),
1✔
1299
            geo_transform: central_geo_transform,
1✔
1300
        };
1✔
1301

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

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

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

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

1✔
1394
        assert!(!grid.is_empty());
1✔
1395

1396
        let grid = grid.into_materialized_masked_grid();
1✔
1397

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

1409
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
1410
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
1411

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

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

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

1✔
1448
        assert!(!grid.is_empty());
1✔
1449

1450
        let x = grid.into_materialized_masked_grid();
1✔
1451

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

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

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

1✔
1483
        assert!(!grid.is_empty());
1✔
1484

1485
        let x = grid.into_materialized_masked_grid();
1✔
1486

1✔
1487
        assert_eq!(x.inner_grid.data.len(), 64);
1✔
1488
        assert_eq!(x.inner_grid.data, &[1; 64]);
1✔
1489
    }
1✔
1490

1491
    #[tokio::test]
1✔
1492
    async fn test_query_single_time_slice() {
1✔
1493
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1494
        let query_ctx = MockQueryContext::test_default();
1✔
1495
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1496

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

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

1✔
1513
        assert_eq!(c.len(), 4);
1✔
1514

1515
        assert_eq!(
1✔
1516
            c[0].time,
1✔
1517
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1518
        );
1✔
1519

1520
        assert_eq!(
1✔
1521
            c[0].tile_information().global_tile_position(),
1✔
1522
            [-1, -1].into()
1✔
1523
        );
1✔
1524

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

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

1535
        assert_eq!(
1✔
1536
            c[3].tile_information().global_tile_position(),
1✔
1537
            [0, 0].into()
1✔
1538
        );
1✔
1539
    }
1540

1541
    #[tokio::test]
1✔
1542
    async fn test_query_multi_time_slices() {
1✔
1543
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1544
        let query_ctx = MockQueryContext::test_default();
1✔
1545
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1546

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

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

1✔
1563
        assert_eq!(c.len(), 8);
1✔
1564

1565
        assert_eq!(
1✔
1566
            c[0].time,
1✔
1567
            TimeInterval::new_unchecked(1_388_534_400_000, 1_391_212_800_000)
1✔
1568
        );
1✔
1569

1570
        assert_eq!(
1✔
1571
            c[5].time,
1✔
1572
            TimeInterval::new_unchecked(1_391_212_800_000, 1_393_632_000_000)
1✔
1573
        );
1✔
1574
    }
1575

1576
    #[tokio::test]
1✔
1577
    async fn test_query_before_data() {
1✔
1578
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1579
        let query_ctx = MockQueryContext::test_default();
1✔
1580
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1581

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

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

1✔
1598
        assert_eq!(c.len(), 4);
1✔
1599

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

1606
    #[tokio::test]
1✔
1607
    async fn test_query_after_data() {
1✔
1608
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1609
        let query_ctx = MockQueryContext::test_default();
1✔
1610
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1611

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

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

1✔
1628
        assert_eq!(c.len(), 4);
1✔
1629

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

1636
    #[tokio::test]
1✔
1637
    async fn test_nodata() {
1✔
1638
        hide_gdal_errors();
1✔
1639

1✔
1640
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1641
        let query_ctx = MockQueryContext::test_default();
1✔
1642
        let id = add_ndvi_dataset(&mut exe_ctx);
1✔
1643

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

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

1✔
1660
        assert_eq!(c.len(), 4);
1✔
1661

1662
        let tile_1 = &c[0];
1✔
1663

1✔
1664
        assert_eq!(
1✔
1665
            tile_1.time,
1✔
1666
            TimeInterval::new_unchecked(TimeInstance::MIN, 1_388_534_400_000)
1✔
1667
        );
1✔
1668

1669
        assert!(tile_1.is_empty());
1✔
1670
    }
1671

1672
    #[tokio::test]
1✔
1673
    async fn timestep_without_params() {
1✔
1674
        let output_bounds =
1✔
1675
            SpatialPartition2D::new_unchecked((-90., 90.).into(), (90., -90.).into());
1✔
1676
        let output_shape: GridShape2D = [256, 256].into();
1✔
1677

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

1682
        let tile = GdalRasterLoader::load_tile_async::<f64>(params, tile_info, time_interval).await;
1✔
1683

1684
        assert!(tile.is_ok());
1✔
1685

1686
        let expected = RasterTile2D::<f64>::new_with_tile_info(
1✔
1687
            time_interval,
1✔
1688
            tile_info,
1✔
1689
            EmptyGrid2D::new(output_shape).into(),
1✔
1690
        );
1✔
1691

1✔
1692
        assert_eq!(tile.unwrap(), expected);
1✔
1693
    }
1694

1695
    #[test]
1✔
1696
    fn it_reverts_config_options() {
1✔
1697
        let config_options = vec![("foo".to_owned(), "bar".to_owned())];
1✔
1698

1✔
1699
        {
1✔
1700
            let _config =
1✔
1701
                TemporaryGdalThreadLocalConfigOptions::new(config_options.as_slice()).unwrap();
1✔
1702

1✔
1703
            assert_eq!(
1✔
1704
                gdal::config::get_config_option("foo", "default").unwrap(),
1✔
1705
                "bar".to_owned()
1✔
1706
            );
1✔
1707
        }
1708

1709
        assert_eq!(
1✔
1710
            gdal::config::get_config_option("foo", "").unwrap(),
1✔
1711
            String::new()
1✔
1712
        );
1✔
1713
    }
1✔
1714

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

1✔
1759
        let dataset_parameters_json = serde_json::to_value(&dataset_parameters).unwrap();
1✔
1760

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

1807
        let deserialized_parameters =
1✔
1808
            serde_json::from_value::<GdalDatasetParameters>(dataset_parameters_json).unwrap();
1✔
1809

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

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

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

1✔
1856
        let sb = gt.spatial_partition(10, 10);
1✔
1857

1✔
1858
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 0.), Coordinate2D::new(10., -10.))
1✔
1859
            .unwrap();
1✔
1860

1✔
1861
        assert_eq!(sb, exp);
1✔
1862
    }
1✔
1863

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

1✔
1872
        let sb = gt.spatial_partition(10, 10);
1✔
1873

1✔
1874
        let exp =
1✔
1875
            SpatialPartition2D::new(Coordinate2D::new(5., 5.), Coordinate2D::new(10., 0.)).unwrap();
1✔
1876

1✔
1877
        assert_eq!(sb, exp);
1✔
1878
    }
1✔
1879

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

1✔
1888
        let sb = gt.spatial_partition(10, 10);
1✔
1889

1✔
1890
        let exp = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
1891
            .unwrap();
1✔
1892

1✔
1893
        assert_eq!(sb, exp);
1✔
1894
    }
1✔
1895

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

1✔
1904
        let sb = gt.spatial_partition(10, 10);
1✔
1905

1✔
1906
        let exp = SpatialPartition2D::new(Coordinate2D::new(5., 0.), Coordinate2D::new(10., -5.))
1✔
1907
            .unwrap();
1✔
1908

1✔
1909
        assert_eq!(sb, exp);
1✔
1910
    }
1✔
1911

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

1✔
1920
        let sb = SpatialPartition2D::new(Coordinate2D::new(8., -7.), Coordinate2D::new(10., -10.))
1✔
1921
            .unwrap();
1✔
1922

1✔
1923
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
1924

1✔
1925
        let exp = GdalReadWindow {
1✔
1926
            read_size_x: 4,
1✔
1927
            read_size_y: 6,
1✔
1928
            read_start_x: 6,
1✔
1929
            read_start_y: 4,
1✔
1930
        };
1✔
1931

1✔
1932
        assert_eq!(rw, exp);
1✔
1933
    }
1✔
1934

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

1✔
1943
        let sb = SpatialPartition2D::new(Coordinate2D::new(0., 10.), Coordinate2D::new(10., 0.))
1✔
1944
            .unwrap();
1✔
1945

1✔
1946
        let rw = gt.spatial_partition_to_read_window(&sb);
1✔
1947

1✔
1948
        let exp = GdalReadWindow {
1✔
1949
            read_size_x: 10,
1✔
1950
            read_size_y: 10,
1✔
1951
            read_start_x: 0,
1✔
1952
            read_start_y: 0,
1✔
1953
        };
1✔
1954

1✔
1955
        assert_eq!(rw, exp);
1✔
1956
    }
1✔
1957

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

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

1✔
2008
        let tile_information =
1✔
2009
            TileInformation::with_partition_and_shape(output_bounds, output_shape);
1✔
2010

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

1✔
2024
        assert!(!grid.is_empty());
1✔
2025

2026
        let grid = grid.into_materialized_masked_grid();
1✔
2027

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

2039
        assert_eq!(grid.validity_mask.data.len(), 64);
1✔
2040
        assert_eq!(grid.validity_mask.data, &[true; 64]);
1✔
2041
    }
1✔
2042
}
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