Skip to content

Commit 6b173a9

Browse files
authored
Merge branch 'main' into workspace-dependencies
2 parents f6a6d1e + 8293ceb commit 6b173a9

File tree

10 files changed

+406
-91
lines changed

10 files changed

+406
-91
lines changed

datatypes/src/primitives/db_types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ impl From<&RasterColorizer> for RasterColorizerDbType {
257257
blue_band,
258258
rgb_params: rgba_params,
259259
} => Self {
260-
r#type: RasterColorizerDbTypeType::SingleBand,
260+
r#type: RasterColorizerDbTypeType::MultiBand,
261261
band: None,
262262
band_colorizer: None,
263263
red_band: Some(i64::from(*red_band)),

operators/src/util/raster_stream_to_png.rs

Lines changed: 59 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use geoengine_datatypes::operations::image::{ColorMapper, RgbParams};
88
use geoengine_datatypes::raster::{FromIndexFn, GridIndexAccess, GridShapeAccess};
99
use geoengine_datatypes::{
1010
operations::image::{Colorizer, RasterColorizer, RgbaColor, ToPng},
11-
primitives::{
12-
AxisAlignedRectangle, BandSelection, CacheHint, RasterQueryRectangle, TimeInterval,
13-
},
11+
primitives::{AxisAlignedRectangle, CacheHint, RasterQueryRectangle, TimeInterval},
1412
raster::{Blit, ConvertDataType, EmptyGrid2D, GeoTransform, GridOrEmpty, Pixel, RasterTile2D},
1513
};
1614
use num_traits::AsPrimitive;
@@ -23,14 +21,24 @@ use tracing::{span, Level};
2321
#[allow(clippy::too_many_arguments)]
2422
pub async fn raster_stream_to_png_bytes<T: Pixel, C: QueryContext + 'static>(
2523
processor: Box<dyn RasterQueryProcessor<RasterType = T>>,
26-
mut query_rect: RasterQueryRectangle,
24+
query_rect: RasterQueryRectangle,
2725
mut query_ctx: C,
2826
width: u32,
2927
height: u32,
3028
time: Option<TimeInterval>,
3129
raster_colorizer: Option<RasterColorizer>,
3230
conn_closed: BoxFuture<'_, ()>,
3331
) -> Result<(Vec<u8>, CacheHint)> {
32+
debug_assert!(
33+
query_rect.attributes.count() <= 3
34+
|| query_rect
35+
.attributes
36+
.as_slice()
37+
.windows(2)
38+
.all(|w| w[0] < w[1]), // TODO: replace with `is_sorted` once it is stable
39+
"bands must be sorted and at most three bands can be queried"
40+
);
41+
3442
let span = span!(Level::TRACE, "raster_stream_to_png_bytes");
3543
let _enter = span.enter();
3644

@@ -52,21 +60,24 @@ pub async fn raster_stream_to_png_bytes<T: Pixel, C: QueryContext + 'static>(
5260
RasterColorizer::SingleBand { band, .. } => vec![band],
5361
};
5462

55-
if query_rect
56-
.attributes
57-
.as_slice()
63+
let band_positions = required_bands
5864
.iter()
59-
.any(|band| !required_bands.contains(band))
60-
{
65+
.filter_map(|band| {
66+
query_rect
67+
.attributes
68+
.as_slice()
69+
.iter()
70+
.position(|b| b == band)
71+
})
72+
.collect::<Vec<usize>>();
73+
74+
if band_positions.len() != required_bands.len() {
6175
return Err(PngCreationError::ColorizerBandsMustBePresentInQuery {
6276
bands_present: query_rect.attributes.as_vec(),
6377
required_bands,
6478
})?;
6579
}
6680

67-
// modify query to only query the required bands
68-
query_rect.attributes = BandSelection::new_unchecked(required_bands);
69-
7081
let query_abort_trigger = query_ctx.abort_trigger()?;
7182

7283
let x_query_resolution = query_rect.spatial_bounds.size_x() / f64::from(width);
@@ -114,6 +125,7 @@ pub async fn raster_stream_to_png_bytes<T: Pixel, C: QueryContext + 'static>(
114125
width,
115126
height,
116127
rgba_params,
128+
band_positions,
117129
conn_closed,
118130
query_abort_trigger,
119131
)
@@ -160,45 +172,45 @@ async fn multi_band_colorizer_to_png_bytes<T: Pixel, C: QueryContext + 'static>(
160172
width: u32,
161173
height: u32,
162174
rgb_params: RgbParams,
175+
band_positions: Vec<usize>,
163176
conn_closed: BoxFuture<'_, ()>,
164177
query_abort_trigger: QueryAbortTrigger,
165178
) -> Result<(Vec<u8>, CacheHint)> {
166-
const RGB_CHANNEL_COUNT: usize = 3;
167-
168-
debug_assert_eq!(query_rect.attributes.count(), RGB_CHANNEL_COUNT as u32);
169-
170-
let tile_stream = processor.query(query_rect.clone(), &query_ctx).await?;
171-
172-
let band_numbers: [u32; 3] = [
173-
query_rect.attributes.as_slice()[0],
174-
query_rect.attributes.as_slice()[1],
175-
query_rect.attributes.as_slice()[2],
176-
];
177-
179+
let rgb_channel_count = query_rect.attributes.count() as usize;
178180
let no_data_color = rgb_params.no_data_color;
179-
180181
let tile_template: RasterTile2D<u32> = tile_template.convert_data_type();
181-
let output_tile = Box::pin(
182-
tile_stream
183-
// .map_ok(|tile| tile.convert_data_type())
184-
.try_chunks(RGB_CHANNEL_COUNT)
185-
.fold(Ok(tile_template), |raster2d, chunk| async move {
186-
let chunk = chunk.boxed_context(error::QueryDidNotProduceNextChunk)?;
182+
let red_band_index = band_positions[0];
183+
let green_band_index = band_positions[1];
184+
let blue_band_index = band_positions[2];
187185

188-
let mut tuple: [RasterTile2D<T>; RGB_CHANNEL_COUNT] = chunk
189-
.try_into()
190-
.map_err(|_| PngCreationError::RgbChunkIsNotThreeBands)?;
186+
let tile_stream = processor.query(query_rect.clone(), &query_ctx).await?;
191187

192-
// tiles don't arrive in query order, so we need to sort them to be in query order
193-
// TODO: remove this when we have a guarantee that tiles are in query order
194-
sort_by_indices(&mut tuple, &band_numbers);
188+
let output_tile = Box::pin(tile_stream.try_chunks(rgb_channel_count).fold(
189+
Ok(tile_template),
190+
|raster2d, chunk| async move {
191+
let chunk = chunk.boxed_context(error::QueryDidNotProduceNextChunk)?;
195192

196-
// TODO: spawn blocking task
197-
let rgb_tile = compute_rgb_tile(tuple, &rgb_params);
193+
if chunk.len() != rgb_channel_count {
194+
return Err(PngCreationError::RgbChunkIsNotEnoughBands)?;
195+
}
198196

199-
blit_tile(raster2d, Ok(rgb_tile))
200-
}),
201-
);
197+
// TODO: spawn blocking task
198+
let rgb_tile = crate::util::spawn_blocking(move || {
199+
compute_rgb_tile(
200+
[
201+
&chunk[red_band_index],
202+
&chunk[green_band_index],
203+
&chunk[blue_band_index],
204+
],
205+
&rgb_params,
206+
)
207+
})
208+
.await
209+
.boxed_context(error::UnexpectedComputational)?;
210+
211+
blit_tile(raster2d, Ok(rgb_tile))
212+
},
213+
));
202214

203215
let result = abortable_query_execution(output_tile, conn_closed, query_abort_trigger).await?;
204216
Ok((
@@ -209,24 +221,6 @@ async fn multi_band_colorizer_to_png_bytes<T: Pixel, C: QueryContext + 'static>(
209221
))
210222
}
211223

212-
/// Sorts the tiles by their band numbers and returns them in the order of the band numbers.
213-
/// Uses bubble sort since the number of bands is small.
214-
fn sort_by_indices<T>(tiles: &mut [T; 3], band_numbers: &[u32; 3]) {
215-
debug_assert_eq!(tiles.len(), 3);
216-
debug_assert_eq!(band_numbers.len(), 3);
217-
218-
let mut band_numbers = [band_numbers[0], band_numbers[1], band_numbers[2]];
219-
220-
for r in [2, 1] {
221-
for i in 0..r {
222-
if band_numbers[i] > band_numbers[i + 1] {
223-
tiles.swap(i, i + 1);
224-
band_numbers.swap(i, i + 1);
225-
}
226-
}
227-
}
228-
}
229-
230224
fn blit_tile<T>(
231225
raster2d: Result<RasterTile2D<T>>,
232226
tile: Result<RasterTile2D<T>>,
@@ -271,7 +265,7 @@ pub fn default_colorizer_gradient<T: Pixel>() -> Colorizer {
271265
}
272266

273267
pub fn compute_rgb_tile<P: Pixel>(
274-
[red, green, blue]: [RasterTile2D<P>; 3],
268+
[red, green, blue]: [&RasterTile2D<P>; 3],
275269
params: &RgbParams,
276270
) -> RasterTile2D<u32> {
277271
fn fit_to_interval_0_255(value: f64, min: f64, max: f64, scale: f64) -> u32 {
@@ -347,8 +341,10 @@ pub enum PngCreationError {
347341
},
348342
#[snafu(display("Query did not produce next chunk: {source}"))]
349343
QueryDidNotProduceNextChunk { source: Box<dyn ErrorSource> },
350-
#[snafu(display("RGB chunk is not three bands"))]
351-
RgbChunkIsNotThreeBands,
344+
#[snafu(display("RGB chunk is not enough bands"))]
345+
RgbChunkIsNotEnoughBands,
346+
#[snafu(display("Unexpected computational error"))]
347+
UnexpectedComputationalError { source: Box<dyn ErrorSource> },
352348
}
353349

354350
#[cfg(test)]
@@ -429,21 +425,4 @@ mod tests {
429425
image_bytes.as_slice()
430426
);
431427
}
432-
433-
#[test]
434-
fn it_sorts_by_index() {
435-
let mut values = [2, 1, 3];
436-
let band_numbers = [2, 1, 5];
437-
438-
sort_by_indices(&mut values, &band_numbers);
439-
440-
assert_eq!(values, [1, 2, 3]);
441-
442-
let mut values = [3, 2, 1];
443-
let band_numbers = [5, 2, 1];
444-
445-
sort_by_indices(&mut values, &band_numbers);
446-
447-
assert_eq!(values, [1, 2, 3]);
448-
}
449428
}

services/src/api/handlers/wms.rs

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,8 @@ async fn wms_map_handler<C: ApplicationContext>(
348348

349349
let attributes = raster_colorizer.as_ref().map_or_else(
350350
|| BandSelection::new_single(0),
351-
|o| {
352-
RasterColorizer::band_selection(o)
351+
|colorizer: &RasterColorizer| {
352+
RasterColorizer::band_selection(colorizer)
353353
.try_into()
354354
.expect("conversion of usize to u32 succeeds for small band numbers")
355355
},
@@ -482,7 +482,8 @@ mod tests {
482482
use crate::ge_context;
483483
use crate::util::tests::{
484484
check_allowed_http_methods, read_body_string, register_ndvi_workflow_helper,
485-
register_ndvi_workflow_helper_with_cache_ttl, send_test_request,
485+
register_ndvi_workflow_helper_with_cache_ttl, register_ne2_multiband_workflow,
486+
send_test_request,
486487
};
487488
use actix_http::header::{self, CONTENT_TYPE};
488489
use actix_web::dev::ServiceResponse;
@@ -861,6 +862,143 @@ mod tests {
861862
);
862863
}
863864

865+
#[ge_context::test(tiling_spec = "get_map_test_helper_tiling_spec")]
866+
async fn it_supports_multiband_colorizer(app_ctx: PostgresContext<NoTls>) {
867+
let ctx = app_ctx.default_session_context().await.unwrap();
868+
869+
let session_id = ctx.session().id();
870+
871+
let (_, id) = register_ne2_multiband_workflow(&app_ctx).await;
872+
873+
let raster_colorizer = RasterColorizer::MultiBand {
874+
red_band: 2,
875+
red_min: 0.,
876+
red_max: 255.,
877+
red_scale: 1.0,
878+
green_band: 1,
879+
green_min: 0.,
880+
green_max: 255.,
881+
green_scale: 1.0,
882+
blue_band: 0,
883+
blue_min: 0.,
884+
blue_max: 255.,
885+
blue_scale: 1.0,
886+
no_data_color: RgbaColor::transparent().into(),
887+
};
888+
889+
let params = &[
890+
("request", "GetMap"),
891+
("service", "WMS"),
892+
("version", "1.3.0"),
893+
("layers", &id.to_string()),
894+
("bbox", "-90,-180,90,180"),
895+
("width", "600"),
896+
("height", "300"),
897+
("crs", "EPSG:4326"),
898+
(
899+
"styles",
900+
&format!(
901+
"custom:{}",
902+
serde_json::to_string(&raster_colorizer).unwrap()
903+
),
904+
),
905+
("format", "image/png"),
906+
("time", "2022-01-01T00:00:00.0Z"),
907+
];
908+
909+
let req = actix_web::test::TestRequest::get()
910+
.uri(&format!(
911+
"/wms/{}?{}",
912+
id,
913+
serde_urlencoded::to_string(params).unwrap()
914+
))
915+
.append_header((header::AUTHORIZATION, Bearer::new(session_id.to_string())));
916+
let res = send_test_request(req, app_ctx).await;
917+
918+
assert_eq!(res.status(), 200);
919+
920+
let image_bytes = actix_web::test::read_body(res).await;
921+
922+
// geoengine_datatypes::util::test::save_test_bytes(&image_bytes, "ne2_rgb_colorizer.png");
923+
924+
assert_eq!(
925+
include_bytes!("../../../../test_data/wms/ne2_rgb_colorizer.png") as &[u8],
926+
image_bytes
927+
);
928+
}
929+
930+
#[ge_context::test(tiling_spec = "get_map_test_helper_tiling_spec")]
931+
async fn it_supports_multiband_colorizer_with_less_then_3_bands(
932+
app_ctx: PostgresContext<NoTls>,
933+
) {
934+
let ctx = app_ctx.default_session_context().await.unwrap();
935+
936+
let session_id = ctx.session().id();
937+
938+
let (_, id) = register_ne2_multiband_workflow(&app_ctx).await;
939+
940+
let raster_colorizer = RasterColorizer::MultiBand {
941+
red_band: 1,
942+
red_min: 0.,
943+
red_max: 255.,
944+
red_scale: 1.0,
945+
green_band: 1,
946+
green_min: 0.,
947+
green_max: 255.,
948+
green_scale: 1.0,
949+
blue_band: 1,
950+
blue_min: 0.,
951+
blue_max: 255.,
952+
blue_scale: 1.0,
953+
no_data_color: RgbaColor::transparent().into(),
954+
};
955+
956+
let params = &[
957+
("request", "GetMap"),
958+
("service", "WMS"),
959+
("version", "1.3.0"),
960+
("layers", &id.to_string()),
961+
("bbox", "-90,-180,90,180"),
962+
("width", "600"),
963+
("height", "300"),
964+
("crs", "EPSG:4326"),
965+
(
966+
"styles",
967+
&format!(
968+
"custom:{}",
969+
serde_json::to_string(&raster_colorizer).unwrap()
970+
),
971+
),
972+
("format", "image/png"),
973+
("time", "2022-01-01T00:00:00.0Z"),
974+
];
975+
976+
let req = actix_web::test::TestRequest::get()
977+
.uri(&format!(
978+
"/wms/{}?{}",
979+
id,
980+
serde_urlencoded::to_string(params).unwrap()
981+
))
982+
.append_header((header::AUTHORIZATION, Bearer::new(session_id.to_string())));
983+
let res = send_test_request(req, app_ctx).await;
984+
985+
assert_eq!(res.status(), 200);
986+
987+
let image_bytes = actix_web::test::read_body(res).await;
988+
989+
// geoengine_datatypes::util::test::save_test_bytes(
990+
// &image_bytes,
991+
// geoengine_datatypes::test_data!("wms/ne2_rgb_colorizer_gray.png")
992+
// .to_str()
993+
// .unwrap(),
994+
// );
995+
996+
assert_eq!(
997+
include_bytes!("../../../../test_data/wms/ne2_rgb_colorizer_gray.png") as &[u8],
998+
image_bytes
999+
);
1000+
}
1001+
8641002
#[ge_context::test]
8651003
async fn it_zoomes_very_far(app_ctx: PostgresContext<NoTls>) {
8661004
let ctx = app_ctx.default_session_context().await.unwrap();

0 commit comments

Comments
 (0)