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

geo-engine / geoengine / 5006008836

pending completion
5006008836

push

github

GitHub
Merge #785 #787

936 of 936 new or added lines in 50 files covered. (100.0%)

96010 of 107707 relevant lines covered (89.14%)

72676.46 hits per line

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

92.69
/operators/src/processing/rasterization/mod.rs
1
use crate::engine::TypedVectorQueryProcessor::MultiPoint;
2
use crate::engine::{
3
    CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources,
4
    InitializedVectorOperator, Operator, OperatorName, QueryContext, QueryProcessor,
5
    RasterOperator, RasterQueryProcessor, RasterResultDescriptor, SingleVectorSource,
6
    TypedRasterQueryProcessor, TypedVectorQueryProcessor, WorkflowOperatorPath,
7
};
8
use arrow::datatypes::ArrowNativeTypeOp;
9

10
use crate::error;
11
use crate::processing::rasterization::GridOrDensity::Grid;
12
use crate::util;
13

14
use async_trait::async_trait;
15

16
use futures::stream::BoxStream;
17
use futures::{stream, StreamExt};
18
use geoengine_datatypes::collections::GeometryCollection;
19

20
use geoengine_datatypes::primitives::{
21
    AxisAlignedRectangle, BoundingBox2D, Coordinate2D, Measurement, RasterQueryRectangle,
22
    SpatialPartition2D, SpatialPartitioned, SpatialResolution, VectorQueryRectangle,
23
};
24
use geoengine_datatypes::raster::{
25
    GeoTransform, Grid2D, GridOrEmpty, GridSize, GridSpaceToLinearSpace, RasterDataType,
26
    RasterTile2D, TilingSpecification,
27
};
28

29
use num_traits::FloatConst;
30
use rayon::prelude::*;
31

32
use serde::{Deserialize, Serialize};
33
use snafu::ensure;
34

35
use crate::util::{spawn_blocking, spawn_blocking_with_thread_pool};
36

37
use typetag::serde;
38

39
/// An operator that rasterizes vector data
40
pub type Rasterization = Operator<GridOrDensity, SingleVectorSource>;
41

42
impl OperatorName for Rasterization {
43
    const TYPE_NAME: &'static str = "Rasterization";
44
}
45

46
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
6✔
47
#[serde(rename_all = "camelCase")]
48
pub enum GridSizeMode {
49
    /// The spatial resolution is interpreted as a fixed size in coordinate units
50
    Fixed,
51
    /// The spatial resolution is interpreted as a multiplier for the query pixel size
52
    Relative,
53
}
54

55
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
8✔
56
#[serde(rename_all = "camelCase")]
57
#[serde(tag = "type")]
58
pub enum GridOrDensity {
59
    /// A grid which summarizes points in cells (2D histogram)
60
    Grid(GridParams),
61
    /// A heatmap calculated from a gaussian density function
62
    Density(DensityParams),
63
}
64

65
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2✔
66
pub struct DensityParams {
67
    /// Defines the cutoff (as percentage of maximum density) down to which a point is taken
68
    /// into account for an output pixel density value
69
    cutoff: f64,
70
    /// The standard deviation parameter for the gaussian function
71
    stddev: f64,
72
}
73

74
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
6✔
75
#[serde(rename_all = "camelCase")]
76
pub struct GridParams {
77
    /// The size of grid cells, interpreted depending on the chosen grid size mode
78
    spatial_resolution: SpatialResolution,
79
    /// The origin coordinate which aligns the grid bounds
80
    origin_coordinate: Coordinate2D,
81
    /// Determines how to interpret the grid resolution
82
    grid_size_mode: GridSizeMode,
83
}
84

85
#[typetag::serde]
×
86
#[async_trait]
87
impl RasterOperator for Rasterization {
88
    async fn _initialize(
8✔
89
        self: Box<Self>,
8✔
90
        path: WorkflowOperatorPath,
8✔
91
        context: &dyn ExecutionContext,
8✔
92
    ) -> util::Result<Box<dyn InitializedRasterOperator>> {
8✔
93
        let name = CanonicOperatorName::from(&self);
8✔
94

95
        let initialized_source = self.sources.initialize_sources(path, context).await?;
8✔
96
        let vector_source = initialized_source.vector;
8✔
97
        let in_desc = vector_source.result_descriptor();
8✔
98

8✔
99
        let tiling_specification = context.tiling_specification();
8✔
100

8✔
101
        let out_desc = RasterResultDescriptor {
8✔
102
            spatial_reference: in_desc.spatial_reference,
8✔
103
            data_type: RasterDataType::F64,
8✔
104
            measurement: Measurement::default(),
8✔
105
            bbox: None,
8✔
106
            time: in_desc.time,
8✔
107
            resolution: None,
8✔
108
        };
8✔
109

8✔
110
        match self.params {
8✔
111
            Grid(params) => Ok(InitializedGridRasterization {
6✔
112
                name,
6✔
113
                source: vector_source,
6✔
114
                result_descriptor: out_desc,
6✔
115
                spatial_resolution: params.spatial_resolution,
6✔
116
                grid_size_mode: params.grid_size_mode,
6✔
117
                tiling_specification,
6✔
118
                origin_coordinate: params.origin_coordinate,
6✔
119
            }
6✔
120
            .boxed()),
6✔
121
            GridOrDensity::Density(params) => InitializedDensityRasterization::new(
2✔
122
                name,
2✔
123
                vector_source,
2✔
124
                out_desc,
2✔
125
                tiling_specification,
2✔
126
                params.cutoff,
2✔
127
                params.stddev,
2✔
128
            )
2✔
129
            .map(InitializedRasterOperator::boxed),
2✔
130
        }
131
    }
16✔
132

133
    span_fn!(Rasterization);
×
134
}
135

136
pub struct InitializedGridRasterization {
137
    name: CanonicOperatorName,
138
    source: Box<dyn InitializedVectorOperator>,
139
    result_descriptor: RasterResultDescriptor,
140
    spatial_resolution: SpatialResolution,
141
    grid_size_mode: GridSizeMode,
142
    tiling_specification: TilingSpecification,
143
    origin_coordinate: Coordinate2D,
144
}
145

146
impl InitializedRasterOperator for InitializedGridRasterization {
147
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
148
        &self.result_descriptor
×
149
    }
×
150

151
    fn query_processor(&self) -> util::Result<TypedRasterQueryProcessor> {
6✔
152
        Ok(TypedRasterQueryProcessor::F64(
6✔
153
            GridRasterizationQueryProcessor {
6✔
154
                input: self.source.query_processor()?,
6✔
155
                spatial_resolution: self.spatial_resolution,
6✔
156
                grid_size_mode: self.grid_size_mode,
6✔
157
                tiling_specification: self.tiling_specification,
6✔
158
                origin_coordinate: self.origin_coordinate,
6✔
159
            }
6✔
160
            .boxed(),
6✔
161
        ))
162
    }
6✔
163

164
    fn canonic_name(&self) -> CanonicOperatorName {
×
165
        self.name.clone()
×
166
    }
×
167
}
168

169
pub struct InitializedDensityRasterization {
170
    name: CanonicOperatorName,
171
    source: Box<dyn InitializedVectorOperator>,
172
    result_descriptor: RasterResultDescriptor,
173
    tiling_specification: TilingSpecification,
174
    radius: f64,
175
    stddev: f64,
176
}
177

178
impl InitializedDensityRasterization {
179
    fn new(
2✔
180
        name: CanonicOperatorName,
2✔
181
        source: Box<dyn InitializedVectorOperator>,
2✔
182
        result_descriptor: RasterResultDescriptor,
2✔
183
        tiling_specification: TilingSpecification,
2✔
184
        cutoff: f64,
2✔
185
        stddev: f64,
2✔
186
    ) -> Result<Self, error::Error> {
2✔
187
        ensure!(
2✔
188
            (0. ..1.).contains(&cutoff),
2✔
189
            error::InvalidOperatorSpec {
×
190
                reason: "The cutoff for density rasterization must be in [0, 1).".to_string()
×
191
            }
×
192
        );
193
        ensure!(
2✔
194
            stddev >= 0.,
2✔
195
            error::InvalidOperatorSpec {
×
196
                reason: "The standard deviation for density rasterization must be greater than or equal to zero."
×
197
                    .to_string()
×
198
            }
×
199
        );
200

201
        // Determine radius from cutoff percentage
202
        let radius = gaussian_inverse(cutoff * gaussian(0., stddev), stddev);
2✔
203

2✔
204
        Ok(InitializedDensityRasterization {
2✔
205
            name,
2✔
206
            source,
2✔
207
            result_descriptor,
2✔
208
            tiling_specification,
2✔
209
            radius,
2✔
210
            stddev,
2✔
211
        })
2✔
212
    }
2✔
213
}
214

215
impl InitializedRasterOperator for InitializedDensityRasterization {
216
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
217
        &self.result_descriptor
×
218
    }
×
219

220
    fn query_processor(&self) -> util::Result<TypedRasterQueryProcessor> {
2✔
221
        Ok(TypedRasterQueryProcessor::F64(
2✔
222
            DensityRasterizationQueryProcessor {
2✔
223
                input: self.source.query_processor()?,
2✔
224
                tiling_specification: self.tiling_specification,
2✔
225
                radius: self.radius,
2✔
226
                stddev: self.stddev,
2✔
227
            }
2✔
228
            .boxed(),
2✔
229
        ))
230
    }
2✔
231

232
    fn canonic_name(&self) -> CanonicOperatorName {
×
233
        self.name.clone()
×
234
    }
×
235
}
236

237
pub struct GridRasterizationQueryProcessor {
238
    input: TypedVectorQueryProcessor,
239
    spatial_resolution: SpatialResolution,
240
    grid_size_mode: GridSizeMode,
241
    tiling_specification: TilingSpecification,
242
    origin_coordinate: Coordinate2D,
243
}
244

245
#[async_trait]
246
impl RasterQueryProcessor for GridRasterizationQueryProcessor {
247
    type RasterType = f64;
248

249
    /// Performs a grid rasterization by first determining the grid resolution to use.
250
    /// The grid resolution is limited to the query resolution, because a finer granularity
251
    /// would not be visible in the resulting raster.
252
    /// Then, for each tile, a grid, aligned to the configured origin coordinate, is created.
253
    /// All points within the spatial bounds of the grid are queried and counted in the
254
    /// grid cells.
255
    /// Finally, the grid resolution is upsampled (if necessary) to the tile resolution.
256
    async fn raster_query<'a>(
6✔
257
        &'a self,
6✔
258
        query: RasterQueryRectangle,
6✔
259
        ctx: &'a dyn QueryContext,
6✔
260
    ) -> util::Result<BoxStream<'a, util::Result<RasterTile2D<Self::RasterType>>>> {
6✔
261
        if let MultiPoint(points_processor) = &self.input {
6✔
262
            let grid_resolution = match self.grid_size_mode {
6✔
263
                GridSizeMode::Fixed => SpatialResolution {
3✔
264
                    x: f64::max(self.spatial_resolution.x, query.spatial_resolution.x),
3✔
265
                    y: f64::max(self.spatial_resolution.y, query.spatial_resolution.y),
3✔
266
                },
3✔
267
                GridSizeMode::Relative => SpatialResolution {
3✔
268
                    x: f64::max(
3✔
269
                        self.spatial_resolution.x * query.spatial_resolution.x,
3✔
270
                        query.spatial_resolution.x,
3✔
271
                    ),
3✔
272
                    y: f64::max(
3✔
273
                        self.spatial_resolution.y * query.spatial_resolution.y,
3✔
274
                        query.spatial_resolution.y,
3✔
275
                    ),
3✔
276
                },
3✔
277
            };
278

279
            let tiling_strategy = self
6✔
280
                .tiling_specification
6✔
281
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
6✔
282
            let tile_shape = tiling_strategy.tile_size_in_pixels;
6✔
283

6✔
284
            let tiles = stream::iter(
6✔
285
                tiling_strategy.tile_information_iterator(query.spatial_bounds),
6✔
286
            )
6✔
287
            .then(move |tile_info| async move {
24✔
288
                let grid_spatial_bounds = tile_info
24✔
289
                    .spatial_partition()
24✔
290
                    .snap_to_grid(self.origin_coordinate, grid_resolution);
24✔
291

24✔
292
                let grid_size_x =
24✔
293
                    f64::ceil(grid_spatial_bounds.size_x() / grid_resolution.x) as usize;
24✔
294
                let grid_size_y =
24✔
295
                    f64::ceil(grid_spatial_bounds.size_y() / grid_resolution.y) as usize;
24✔
296

24✔
297
                let vector_query = VectorQueryRectangle {
24✔
298
                    spatial_bounds: grid_spatial_bounds.as_bbox(),
24✔
299
                    time_interval: query.time_interval,
24✔
300
                    spatial_resolution: grid_resolution,
24✔
301
                };
24✔
302

24✔
303
                let grid_geo_transform = GeoTransform::new(
24✔
304
                    grid_spatial_bounds.upper_left(),
24✔
305
                    grid_resolution.x,
24✔
306
                    -grid_resolution.y,
24✔
307
                );
24✔
308

309
                let mut chunks = points_processor.query(vector_query, ctx).await?;
24✔
310

311
                let mut grid_data = vec![0.; grid_size_x * grid_size_y];
24✔
312
                while let Some(chunk) = chunks.next().await {
48✔
313
                    let chunk = chunk?;
24✔
314
                    grid_data = spawn_blocking(move || {
24✔
315
                        for &coord in chunk.coordinates() {
24✔
316
                            if !grid_spatial_bounds.contains_coordinate(&coord) {
24✔
317
                                continue;
3✔
318
                            }
21✔
319
                            let [y, x] = grid_geo_transform.coordinate_to_grid_idx_2d(coord).0;
21✔
320
                            grid_data[x as usize + y as usize * grid_size_x] += 1.;
21✔
321
                        }
322
                        grid_data
24✔
323
                    })
24✔
324
                    .await
24✔
325
                    .expect("Should only forward panics from spawned task");
24✔
326
                }
327

328
                let tile_data = spawn_blocking(move || {
24✔
329
                    let mut tile_data = Vec::with_capacity(tile_shape.number_of_elements());
24✔
330
                    for tile_y in 0..tile_shape.axis_size_y() as isize {
60✔
331
                        for tile_x in 0..tile_shape.axis_size_x() as isize {
156✔
332
                            let pixel_coordinate = tile_info
156✔
333
                                .tile_geo_transform()
156✔
334
                                .grid_idx_to_pixel_center_coordinate_2d([tile_y, tile_x].into());
156✔
335
                            if query.spatial_bounds.contains_coordinate(&pixel_coordinate) {
156✔
336
                                let [grid_y, grid_x] = grid_geo_transform
156✔
337
                                    .coordinate_to_grid_idx_2d(pixel_coordinate)
156✔
338
                                    .0;
156✔
339
                                tile_data.push(
156✔
340
                                    grid_data[grid_x as usize + grid_y as usize * grid_size_x],
156✔
341
                                );
156✔
342
                            } else {
156✔
343
                                tile_data.push(0.);
×
344
                            }
×
345
                        }
346
                    }
347
                    tile_data
24✔
348
                })
24✔
349
                .await
24✔
350
                .expect("Should only forward panics from spawned task");
24✔
351
                let tile_grid = Grid2D::new(tile_shape, tile_data)
24✔
352
                    .expect("Data vector length should match the number of pixels in the tile");
24✔
353

24✔
354
                Ok(RasterTile2D::new_with_tile_info(
24✔
355
                    query.time_interval,
24✔
356
                    tile_info,
24✔
357
                    GridOrEmpty::Grid(tile_grid.into()),
24✔
358
                ))
24✔
359
            });
24✔
360
            Ok(tiles.boxed())
6✔
361
        } else {
362
            Ok(generate_zeroed_tiles(self.tiling_specification, query))
×
363
        }
364
    }
12✔
365
}
366

367
pub struct DensityRasterizationQueryProcessor {
368
    input: TypedVectorQueryProcessor,
369
    tiling_specification: TilingSpecification,
370
    radius: f64,
371
    stddev: f64,
372
}
373

374
#[async_trait]
375
impl RasterQueryProcessor for DensityRasterizationQueryProcessor {
376
    type RasterType = f64;
377

378
    /// Performs a gaussian density rasterization.
379
    /// For each tile, the spatial bounds are extended by `radius` in x and y direction.
380
    /// All points within these extended bounds are then queried. For each point, the distance to
381
    /// its surrounding tile pixels (up to `radius` distance) is measured and input into the
382
    /// gaussian density function with the configured standard deviation. The density values
383
    /// for each pixel are then summed to result in the tile pixel grid.
384
    async fn raster_query<'a>(
2✔
385
        &'a self,
2✔
386
        query: RasterQueryRectangle,
2✔
387
        ctx: &'a dyn QueryContext,
2✔
388
    ) -> util::Result<BoxStream<'a, util::Result<RasterTile2D<Self::RasterType>>>> {
2✔
389
        if let MultiPoint(points_processor) = &self.input {
2✔
390
            let tiling_strategy = self
2✔
391
                .tiling_specification
2✔
392
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
2✔
393

2✔
394
            let tile_size_x = tiling_strategy.tile_size_in_pixels.axis_size_x();
2✔
395
            let tile_size_y = tiling_strategy.tile_size_in_pixels.axis_size_y();
2✔
396

2✔
397
            // Use rounding factor calculated from query resolution to extend in full pixel units
2✔
398
            let rounding_factor = f64::max(
2✔
399
                1. / query.spatial_resolution.x,
2✔
400
                1. / query.spatial_resolution.y,
2✔
401
            );
2✔
402
            let radius = (self.radius * rounding_factor).ceil() / rounding_factor;
2✔
403

2✔
404
            let tiles = stream::iter(
2✔
405
                tiling_strategy.tile_information_iterator(query.spatial_bounds),
2✔
406
            )
2✔
407
            .then(move |tile_info| async move {
4✔
408
                let tile_bounds = tile_info.spatial_partition();
4✔
409

4✔
410
                let vector_query = VectorQueryRectangle {
4✔
411
                    spatial_bounds: extended_bounding_box_from_spatial_partition(
4✔
412
                        tile_bounds,
4✔
413
                        radius,
4✔
414
                    ),
4✔
415
                    time_interval: query.time_interval,
4✔
416
                    spatial_resolution: query.spatial_resolution,
4✔
417
                };
4✔
418

4✔
419
                let tile_geo_transform = tile_info.tile_geo_transform();
4✔
420

421
                let mut chunks = points_processor.query(vector_query, ctx).await?;
4✔
422

423
                let mut tile_data = vec![0.; tile_size_x * tile_size_y];
4✔
424

425
                while let Some(chunk) = chunks.next().await {
8✔
426
                    let chunk = chunk?;
4✔
427
                    let stddev = self.stddev;
4✔
428
                    tile_data =
4✔
429
                        spawn_blocking_with_thread_pool(ctx.thread_pool().clone(), move || {
4✔
430
                            tile_data.par_iter_mut().enumerate().for_each(
4✔
431
                                |(linear_index, pixel)| {
16✔
432
                                    let pixel_coordinate = tile_geo_transform
16✔
433
                                        .grid_idx_to_pixel_center_coordinate_2d(
16✔
434
                                            tile_geo_transform
16✔
435
                                                .spatial_to_grid_bounds(&tile_bounds)
16✔
436
                                                .grid_idx_unchecked(linear_index),
16✔
437
                                        );
16✔
438

439
                                    for coord in chunk.coordinates() {
32✔
440
                                        let distance = coord.euclidean_distance(&pixel_coordinate);
32✔
441

32✔
442
                                        if distance <= radius {
32✔
443
                                            *pixel += gaussian(distance, stddev);
20✔
444
                                        }
20✔
445
                                    }
446
                                },
16✔
447
                            );
4✔
448

4✔
449
                            tile_data
4✔
450
                        })
4✔
451
                        .await?;
4✔
452
                }
453

454
                Ok(RasterTile2D::new_with_tile_info(
4✔
455
                    query.time_interval,
4✔
456
                    tile_info,
4✔
457
                    GridOrEmpty::Grid(
4✔
458
                        Grid2D::new(tiling_strategy.tile_size_in_pixels, tile_data)
4✔
459
                            .expect(
4✔
460
                                "Data vector length should match the number of pixels in the tile",
4✔
461
                            )
4✔
462
                            .into(),
4✔
463
                    ),
4✔
464
                ))
4✔
465
            });
4✔
466

2✔
467
            Ok(tiles.boxed())
2✔
468
        } else {
469
            Ok(generate_zeroed_tiles(self.tiling_specification, query))
×
470
        }
471
    }
4✔
472
}
473

474
fn generate_zeroed_tiles<'a>(
×
475
    tiling_specification: TilingSpecification,
×
476
    query: RasterQueryRectangle,
×
477
) -> BoxStream<'a, util::Result<RasterTile2D<f64>>> {
×
478
    let tiling_strategy =
×
479
        tiling_specification.strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
×
480
    let tile_shape = tiling_strategy.tile_size_in_pixels;
×
481

×
482
    stream::iter(
×
483
        tiling_strategy
×
484
            .tile_information_iterator(query.spatial_bounds)
×
485
            .map(move |tile_info| {
×
486
                let tile_data = vec![0.; tile_shape.number_of_elements()];
×
487
                let tile_grid = Grid2D::new(tile_shape, tile_data)
×
488
                    .expect("Data vector length should match the number of pixels in the tile");
×
489

×
490
                Ok(RasterTile2D::new_with_tile_info(
×
491
                    query.time_interval,
×
492
                    tile_info,
×
493
                    GridOrEmpty::Grid(tile_grid.into()),
×
494
                ))
×
495
            }),
×
496
    )
×
497
    .boxed()
×
498
}
×
499

500
fn extended_bounding_box_from_spatial_partition(
4✔
501
    spatial_partition: SpatialPartition2D,
4✔
502
    extent: f64,
4✔
503
) -> BoundingBox2D {
4✔
504
    BoundingBox2D::new_unchecked(
4✔
505
        Coordinate2D::new(
4✔
506
            spatial_partition
4✔
507
                .lower_left()
4✔
508
                .x
4✔
509
                .sub_checked(extent)
4✔
510
                .unwrap_or(f64::MIN),
4✔
511
            spatial_partition
4✔
512
                .lower_left()
4✔
513
                .y
4✔
514
                .sub_checked(extent)
4✔
515
                .unwrap_or(f64::MIN),
4✔
516
        ),
4✔
517
        Coordinate2D::new(
4✔
518
            spatial_partition
4✔
519
                .upper_right()
4✔
520
                .x
4✔
521
                .add_checked(extent)
4✔
522
                .unwrap_or(f64::MAX),
4✔
523
            spatial_partition
4✔
524
                .upper_right()
4✔
525
                .y
4✔
526
                .add_checked(extent)
4✔
527
                .unwrap_or(f64::MAX),
4✔
528
        ),
4✔
529
    )
4✔
530
}
4✔
531

532
/// Calculates the gaussian density value for
533
/// `x`, the distance from the mean and
534
/// `stddev`, the standard deviation
535
fn gaussian(x: f64, stddev: f64) -> f64 {
46✔
536
    (1. / (f64::sqrt(2. * f64::PI()) * stddev)) * f64::exp(-(x * x) / (2. * stddev * stddev))
46✔
537
}
46✔
538

539
/// The inverse function of [gaussian](gaussian)
540
fn gaussian_inverse(x: f64, stddev: f64) -> f64 {
2✔
541
    f64::sqrt(2.)
2✔
542
        * f64::sqrt(stddev * stddev * f64::ln(1. / (f64::sqrt(2. * f64::PI()) * stddev * x)))
2✔
543
}
2✔
544

545
#[cfg(test)]
546
mod tests {
547
    use crate::engine::{
548
        InitializedRasterOperator, MockExecutionContext, MockQueryContext, QueryProcessor,
549
        RasterOperator, SingleVectorSource, VectorOperator, WorkflowOperatorPath,
550
    };
551
    use crate::mock::{MockPointSource, MockPointSourceParams};
552
    use crate::processing::rasterization::GridSizeMode::{Fixed, Relative};
553
    use crate::processing::rasterization::{
554
        gaussian, DensityParams, GridOrDensity, GridParams, Rasterization,
555
    };
556
    use futures::StreamExt;
557
    use geoengine_datatypes::primitives::{
558
        Coordinate2D, RasterQueryRectangle, SpatialPartition2D, SpatialResolution,
559
    };
560
    use geoengine_datatypes::raster::TilingSpecification;
561
    use geoengine_datatypes::util::test::TestDefault;
562

563
    async fn get_results(
8✔
564
        rasterization: Box<dyn InitializedRasterOperator>,
8✔
565
        query: RasterQueryRectangle,
8✔
566
    ) -> Vec<Vec<f64>> {
8✔
567
        rasterization
8✔
568
            .query_processor()
8✔
569
            .unwrap()
8✔
570
            .get_f64()
8✔
571
            .unwrap()
8✔
572
            .query(query, &MockQueryContext::test_default())
8✔
573
            .await
×
574
            .unwrap()
8✔
575
            .map(|res| {
28✔
576
                res.unwrap()
28✔
577
                    .grid_array
28✔
578
                    .into_materialized_masked_grid()
28✔
579
                    .inner_grid
28✔
580
                    .data
28✔
581
            })
28✔
582
            .collect()
8✔
583
            .await
52✔
584
    }
8✔
585

586
    #[tokio::test]
1✔
587
    async fn fixed_grid_basic() {
1✔
588
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
589
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
590
        );
1✔
591
        let rasterization = Rasterization {
1✔
592
            params: GridOrDensity::Grid(GridParams {
1✔
593
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
594
                origin_coordinate: [0., 0.].into(),
1✔
595
                grid_size_mode: Fixed,
1✔
596
            }),
1✔
597
            sources: SingleVectorSource {
1✔
598
                vector: MockPointSource {
1✔
599
                    params: MockPointSourceParams {
1✔
600
                        points: vec![
1✔
601
                            (-1., 1.).into(),
1✔
602
                            (1., 1.).into(),
1✔
603
                            (-1., -1.).into(),
1✔
604
                            (1., -1.).into(),
1✔
605
                        ],
1✔
606
                    },
1✔
607
                }
1✔
608
                .boxed(),
1✔
609
            },
1✔
610
        }
1✔
611
        .boxed()
1✔
612
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
613
        .await
×
614
        .unwrap();
1✔
615

1✔
616
        let query = RasterQueryRectangle {
1✔
617
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., -2.].into()).unwrap(),
1✔
618
            time_interval: Default::default(),
1✔
619
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
620
        };
1✔
621

622
        let res = get_results(rasterization, query).await;
8✔
623

624
        assert_eq!(
1✔
625
            res,
1✔
626
            vec![
1✔
627
                vec![0., 0., 0., 1.],
1✔
628
                vec![0., 0., 0., 1.],
1✔
629
                vec![0., 0., 0., 1.],
1✔
630
                vec![0., 0., 0., 1.],
1✔
631
            ]
1✔
632
        );
1✔
633
    }
634

635
    #[tokio::test]
1✔
636
    async fn fixed_grid_with_shift() {
1✔
637
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
638
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
639
        );
1✔
640
        let rasterization = Rasterization {
1✔
641
            params: GridOrDensity::Grid(GridParams {
1✔
642
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
643
                origin_coordinate: [0.5, -0.5].into(),
1✔
644
                grid_size_mode: Fixed,
1✔
645
            }),
1✔
646
            sources: SingleVectorSource {
1✔
647
                vector: MockPointSource {
1✔
648
                    params: MockPointSourceParams {
1✔
649
                        points: vec![
1✔
650
                            (-1., 1.).into(),
1✔
651
                            (1., 1.).into(),
1✔
652
                            (-1., -1.).into(),
1✔
653
                            (1., -1.).into(),
1✔
654
                        ],
1✔
655
                    },
1✔
656
                }
1✔
657
                .boxed(),
1✔
658
            },
1✔
659
        }
1✔
660
        .boxed()
1✔
661
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
662
        .await
×
663
        .unwrap();
1✔
664

1✔
665
        let query = RasterQueryRectangle {
1✔
666
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., -2.].into()).unwrap(),
1✔
667
            time_interval: Default::default(),
1✔
668
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
669
        };
1✔
670

671
        let res = get_results(rasterization, query).await;
8✔
672

673
        assert_eq!(
1✔
674
            res,
1✔
675
            vec![
1✔
676
                vec![1., 0., 0., 0.],
1✔
677
                vec![1., 0., 0., 0.],
1✔
678
                vec![1., 0., 0., 0.],
1✔
679
                vec![1., 0., 0., 0.],
1✔
680
            ]
1✔
681
        );
1✔
682
    }
683

684
    #[tokio::test]
1✔
685
    async fn fixed_grid_with_upsampling() {
1✔
686
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
687
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
688
        );
1✔
689
        let rasterization = Rasterization {
1✔
690
            params: GridOrDensity::Grid(GridParams {
1✔
691
                spatial_resolution: SpatialResolution { x: 2.0, y: 2.0 },
1✔
692
                origin_coordinate: [0., 0.].into(),
1✔
693
                grid_size_mode: Fixed,
1✔
694
            }),
1✔
695
            sources: SingleVectorSource {
1✔
696
                vector: MockPointSource {
1✔
697
                    params: MockPointSourceParams {
1✔
698
                        points: vec![
1✔
699
                            (-1., 1.).into(),
1✔
700
                            (1., 1.).into(),
1✔
701
                            (-1., -1.).into(),
1✔
702
                            (1., -1.).into(),
1✔
703
                        ],
1✔
704
                    },
1✔
705
                }
1✔
706
                .boxed(),
1✔
707
            },
1✔
708
        }
1✔
709
        .boxed()
1✔
710
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
711
        .await
×
712
        .unwrap();
1✔
713

1✔
714
        let query = RasterQueryRectangle {
1✔
715
            spatial_bounds: SpatialPartition2D::new([-3., 3.].into(), [3., -3.].into()).unwrap(),
1✔
716
            time_interval: Default::default(),
1✔
717
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
718
        };
1✔
719

720
        let res = get_results(rasterization, query).await;
8✔
721

722
        assert_eq!(
1✔
723
            res,
1✔
724
            vec![
1✔
725
                vec![0., 0., 0., 0., 1., 1., 0., 1., 1.],
1✔
726
                vec![0., 0., 0., 1., 1., 0., 1., 1., 0.],
1✔
727
                vec![0., 1., 1., 0., 1., 1., 0., 0., 0.],
1✔
728
                vec![1., 1., 0., 1., 1., 0., 0., 0., 0.],
1✔
729
            ]
1✔
730
        );
1✔
731
    }
732

733
    #[tokio::test]
1✔
734
    async fn relative_grid_basic() {
1✔
735
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
736
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
737
        );
1✔
738
        let rasterization = Rasterization {
1✔
739
            params: GridOrDensity::Grid(GridParams {
1✔
740
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
741
                origin_coordinate: [0., 0.].into(),
1✔
742
                grid_size_mode: Relative,
1✔
743
            }),
1✔
744
            sources: SingleVectorSource {
1✔
745
                vector: MockPointSource {
1✔
746
                    params: MockPointSourceParams {
1✔
747
                        points: vec![
1✔
748
                            (-1., 1.).into(),
1✔
749
                            (1., 1.).into(),
1✔
750
                            (-1., -1.).into(),
1✔
751
                            (1., -1.).into(),
1✔
752
                        ],
1✔
753
                    },
1✔
754
                }
1✔
755
                .boxed(),
1✔
756
            },
1✔
757
        }
1✔
758
        .boxed()
1✔
759
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
760
        .await
×
761
        .unwrap();
1✔
762

1✔
763
        let query = RasterQueryRectangle {
1✔
764
            spatial_bounds: SpatialPartition2D::new([-1.5, 1.5].into(), [1.5, -1.5].into())
1✔
765
                .unwrap(),
1✔
766
            time_interval: Default::default(),
1✔
767
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
768
        };
1✔
769

770
        let res = get_results(rasterization, query).await;
8✔
771

772
        assert_eq!(
1✔
773
            res,
1✔
774
            vec![
1✔
775
                vec![0., 0., 0., 0., 1., 0., 0., 0., 0.],
1✔
776
                vec![0., 0., 0., 0., 0., 1., 0., 0., 0.],
1✔
777
                vec![0., 0., 0., 0., 0., 0., 0., 1., 0.],
1✔
778
                vec![0., 0., 0., 0., 0., 0., 0., 0., 1.],
1✔
779
            ]
1✔
780
        );
1✔
781
    }
782

783
    #[tokio::test]
1✔
784
    async fn relative_grid_with_shift() {
1✔
785
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
786
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
787
        );
1✔
788
        let rasterization = Rasterization {
1✔
789
            params: GridOrDensity::Grid(GridParams {
1✔
790
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
791
                origin_coordinate: [0.25, -0.25].into(),
1✔
792
                grid_size_mode: Relative,
1✔
793
            }),
1✔
794
            sources: SingleVectorSource {
1✔
795
                vector: MockPointSource {
1✔
796
                    params: MockPointSourceParams {
1✔
797
                        points: vec![
1✔
798
                            (-1., 1.).into(),
1✔
799
                            (1., 1.).into(),
1✔
800
                            (-1., -1.).into(),
1✔
801
                            (1., -1.).into(),
1✔
802
                        ],
1✔
803
                    },
1✔
804
                }
1✔
805
                .boxed(),
1✔
806
            },
1✔
807
        }
1✔
808
        .boxed()
1✔
809
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
810
        .await
×
811
        .unwrap();
1✔
812

1✔
813
        let query = RasterQueryRectangle {
1✔
814
            spatial_bounds: SpatialPartition2D::new([-1.5, 1.5].into(), [1.5, -1.5].into())
1✔
815
                .unwrap(),
1✔
816
            time_interval: Default::default(),
1✔
817
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
818
        };
1✔
819

820
        let res = get_results(rasterization, query).await;
8✔
821

822
        assert_eq!(
1✔
823
            res,
1✔
824
            vec![
1✔
825
                vec![1., 0., 0., 0., 0., 0., 0., 0., 0.],
1✔
826
                vec![0., 1., 0., 0., 0., 0., 0., 0., 0.],
1✔
827
                vec![0., 0., 0., 1., 0., 0., 0., 0., 0.],
1✔
828
                vec![0., 0., 0., 0., 1., 0., 0., 0., 0.],
1✔
829
            ]
1✔
830
        );
1✔
831
    }
832

833
    #[tokio::test]
1✔
834
    async fn relative_grid_with_upsampling() {
1✔
835
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
836
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
837
        );
1✔
838
        let rasterization = Rasterization {
1✔
839
            params: GridOrDensity::Grid(GridParams {
1✔
840
                spatial_resolution: SpatialResolution { x: 2.0, y: 2.0 },
1✔
841
                origin_coordinate: [0., 0.].into(),
1✔
842
                grid_size_mode: Relative,
1✔
843
            }),
1✔
844
            sources: SingleVectorSource {
1✔
845
                vector: MockPointSource {
1✔
846
                    params: MockPointSourceParams {
1✔
847
                        points: vec![
1✔
848
                            (-1., 1.).into(),
1✔
849
                            (1., 1.).into(),
1✔
850
                            (-1., -1.).into(),
1✔
851
                            (1., -1.).into(),
1✔
852
                        ],
1✔
853
                    },
1✔
854
                }
1✔
855
                .boxed(),
1✔
856
            },
1✔
857
        }
1✔
858
        .boxed()
1✔
859
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
860
        .await
×
861
        .unwrap();
1✔
862

1✔
863
        let query = RasterQueryRectangle {
1✔
864
            spatial_bounds: SpatialPartition2D::new([-1., 1.].into(), [1., -1.].into()).unwrap(),
1✔
865
            time_interval: Default::default(),
1✔
866
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
867
        };
1✔
868

869
        let res = get_results(rasterization, query).await;
8✔
870

871
        assert_eq!(
1✔
872
            res,
1✔
873
            vec![
1✔
874
                vec![1., 1., 1., 1.],
1✔
875
                vec![0., 0., 0., 0.],
1✔
876
                vec![0., 0., 0., 0.],
1✔
877
                vec![0., 0., 0., 0.]
1✔
878
            ]
1✔
879
        );
1✔
880
    }
881

882
    #[tokio::test]
1✔
883
    async fn density_basic() {
1✔
884
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
885
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
886
        );
1✔
887
        let rasterization = Rasterization {
1✔
888
            params: GridOrDensity::Density(DensityParams {
1✔
889
                cutoff: gaussian(0.99, 1.0) / gaussian(0., 1.0),
1✔
890
                stddev: 1.0,
1✔
891
            }),
1✔
892
            sources: SingleVectorSource {
1✔
893
                vector: MockPointSource {
1✔
894
                    params: MockPointSourceParams {
1✔
895
                        points: vec![(-1., 1.).into(), (1., 1.).into()],
1✔
896
                    },
1✔
897
                }
1✔
898
                .boxed(),
1✔
899
            },
1✔
900
        }
1✔
901
        .boxed()
1✔
902
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
903
        .await
×
904
        .unwrap();
1✔
905

1✔
906
        let query = RasterQueryRectangle {
1✔
907
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., 0.].into()).unwrap(),
1✔
908
            time_interval: Default::default(),
1✔
909
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
910
        };
1✔
911

912
        let res = get_results(rasterization, query).await;
2✔
913

914
        assert_eq!(
1✔
915
            res,
1✔
916
            vec![
1✔
917
                vec![
1✔
918
                    gaussian(
1✔
919
                        Coordinate2D::new(-1., 1.)
1✔
920
                            .euclidean_distance(&Coordinate2D::new(-1.5, 1.5)),
1✔
921
                        1.0
1✔
922
                    ),
1✔
923
                    gaussian(
1✔
924
                        Coordinate2D::new(-1., 1.)
1✔
925
                            .euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
926
                        1.0
1✔
927
                    ),
1✔
928
                    gaussian(
1✔
929
                        Coordinate2D::new(-1., 1.)
1✔
930
                            .euclidean_distance(&Coordinate2D::new(-1.5, 0.5)),
1✔
931
                        1.0
1✔
932
                    ),
1✔
933
                    gaussian(
1✔
934
                        Coordinate2D::new(-1., 1.)
1✔
935
                            .euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
936
                        1.0
1✔
937
                    )
1✔
938
                ],
1✔
939
                vec![
1✔
940
                    gaussian(
1✔
941
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
942
                        1.0
1✔
943
                    ),
1✔
944
                    gaussian(
1✔
945
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 1.5)),
1✔
946
                        1.0
1✔
947
                    ),
1✔
948
                    gaussian(
1✔
949
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
950
                        1.0
1✔
951
                    ),
1✔
952
                    gaussian(
1✔
953
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 0.5)),
1✔
954
                        1.0
1✔
955
                    )
1✔
956
                ],
1✔
957
            ]
1✔
958
        );
1✔
959
    }
960

961
    #[tokio::test]
1✔
962
    async fn density_radius_overlap() {
1✔
963
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
964
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
965
        );
1✔
966
        let rasterization = Rasterization {
1✔
967
            params: GridOrDensity::Density(DensityParams {
1✔
968
                cutoff: gaussian(1.99, 1.0) / gaussian(0., 1.0),
1✔
969
                stddev: 1.0,
1✔
970
            }),
1✔
971
            sources: SingleVectorSource {
1✔
972
                vector: MockPointSource {
1✔
973
                    params: MockPointSourceParams {
1✔
974
                        points: vec![(-1., 1.).into(), (1., 1.).into()],
1✔
975
                    },
1✔
976
                }
1✔
977
                .boxed(),
1✔
978
            },
1✔
979
        }
1✔
980
        .boxed()
1✔
981
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
982
        .await
×
983
        .unwrap();
1✔
984

1✔
985
        let query = RasterQueryRectangle {
1✔
986
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., 0.].into()).unwrap(),
1✔
987
            time_interval: Default::default(),
1✔
988
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
989
        };
1✔
990

991
        let res = get_results(rasterization, query).await;
2✔
992

993
        assert_eq!(
1✔
994
            res,
1✔
995
            vec![
1✔
996
                vec![
1✔
997
                    gaussian(
1✔
998
                        Coordinate2D::new(-1., 1.)
1✔
999
                            .euclidean_distance(&Coordinate2D::new(-1.5, 1.5)),
1✔
1000
                        1.0
1✔
1001
                    ),
1✔
1002
                    gaussian(
1✔
1003
                        Coordinate2D::new(-1., 1.)
1✔
1004
                            .euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
1005
                        1.0
1✔
1006
                    ) + gaussian(
1✔
1007
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
1008
                        1.0
1✔
1009
                    ),
1✔
1010
                    gaussian(
1✔
1011
                        Coordinate2D::new(-1., 1.)
1✔
1012
                            .euclidean_distance(&Coordinate2D::new(-1.5, 0.5)),
1✔
1013
                        1.0
1✔
1014
                    ),
1✔
1015
                    gaussian(
1✔
1016
                        Coordinate2D::new(-1., 1.)
1✔
1017
                            .euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
1018
                        1.0
1✔
1019
                    ) + gaussian(
1✔
1020
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
1021
                        1.0
1✔
1022
                    )
1✔
1023
                ],
1✔
1024
                vec![
1✔
1025
                    gaussian(
1✔
1026
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
1027
                        1.0
1✔
1028
                    ) + gaussian(
1✔
1029
                        Coordinate2D::new(-1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
1030
                        1.0
1✔
1031
                    ),
1✔
1032
                    gaussian(
1✔
1033
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 1.5)),
1✔
1034
                        1.0
1✔
1035
                    ),
1✔
1036
                    gaussian(
1✔
1037
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
1038
                        1.0
1✔
1039
                    ) + gaussian(
1✔
1040
                        Coordinate2D::new(-1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
1041
                        1.0
1✔
1042
                    ),
1✔
1043
                    gaussian(
1✔
1044
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 0.5)),
1✔
1045
                        1.0
1✔
1046
                    )
1✔
1047
                ],
1✔
1048
            ]
1✔
1049
        );
1✔
1050
    }
1051
}
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

© 2025 Coveralls, Inc