use std::num::NonZero;
use std::sync::LazyLock;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use martin_core::tiles::hillshade::{Canvas, bake_with_light};
use martin_core::tiles::neighbourhood::{
InputEtag, NEIGHBOURHOOD_LEN, Neighbourhood, neighbourhood_etag,
};
use martin_core::tiles::{BoxedSource, MartinCoreError, Tile, TileCache};
use martin_tile_utils::{Encoding, TileCoord, TileInfo};
use tokio::sync::Semaphore;
use tracing::{debug, warn};
use super::ProcessError;
use super::neighbourhood::{GATHER_PERMITS, gather};
use crate::config::file::ResolvedHillshade;
static BAKE_PERMITS: LazyLock<Semaphore> = LazyLock::new(|| {
let cores = std::thread::available_parallelism().map_or(4, NonZero::get);
Semaphore::new(cores)
});
pub async fn bake_hillshade(
source: &BoxedSource,
settings: ResolvedHillshade,
xyz: TileCoord,
cache: Option<&TileCache>,
) -> Result<Tile, ProcessError> {
let slots = {
let _permit = GATHER_PERMITS
.acquire()
.await
.map_err(|_closed| ProcessError::HillshadeShuttingDown)?;
gather(source, xyz, cache).await.map_err(|e| {
if matches!(e.as_ref(), MartinCoreError::SourceNeedsReload) {
ProcessError::HillshadeSourceNeedsReload
} else {
ProcessError::HillshadeSource(e.to_string())
}
})?
};
if slots[Neighbourhood::CENTRE].is_none() {
debug!(
source.id = source.get_id(),
tile = %xyz,
"No normal tile at this coordinate; serving no content rather than a blank bake"
);
return Ok(Tile::new_hash_etag(
Vec::new(),
TileInfo::new(settings.format, Encoding::Internal),
));
}
let etag = {
let inputs: [InputEtag<'_>; NEIGHBOURHOOD_LEN] = std::array::from_fn(|i| {
InputEtag::from_slot(slots[i].as_ref().map(|(_, etag)| etag.as_str()))
});
neighbourhood_etag(&inputs, &settings.fingerprint())
.map(|hash| URL_SAFE_NO_PAD.encode(hash.to_ne_bytes()))
};
let neighbourhood = Neighbourhood::from_row_major(slots.map(|slot| slot.map(|(data, _)| data)));
let format = settings.format;
let encoded = {
let _permit = BAKE_PERMITS
.acquire()
.await
.map_err(|_closed| ProcessError::HillshadeShuttingDown)?;
tokio::task::spawn_blocking(move || {
let canvas = Canvas::from_neighbourhood(&neighbourhood)?;
let baked = bake_with_light(&canvas, 512, &settings.bake, settings.light.to_vector());
baked.encode(format)
})
.await
.map_err(|e| ProcessError::HillshadeBakeFailed(e.to_string()))??
};
let info = TileInfo::new(format, Encoding::Internal);
if etag.is_none() {
warn!(
source.id = source.get_id(),
tile = %xyz,
"A normal tile carries no etag, so the baked hillshade is served without one"
);
}
Ok(Tile::new_with_etag(encoded, info, etag.unwrap_or_default()))
}