use std::sync::Arc;
use oxigis_core::LayerId;
use oxigis_ui::{
ArchiveTileProvider, ArchiveTileTransport, BoxedTileProvider, BoxedVectorTileSource,
CogTileProvider, TileProvider, VectorTileProvider, VectorTileSource, XyzTileProvider,
};
const DEAD_SOURCE_FAILURES: usize = 4;
pub struct InstalledProvider {
pub provider: BoxedTileProvider,
pub watch: RasterWatch,
}
pub struct InstalledVectorSource {
pub source: BoxedVectorTileSource,
pub watch: VectorWatch,
}
#[derive(Default)]
pub struct ProviderWatch {
raster: RasterWatch,
stack: Vec<(LayerId, StackWatch)>,
}
pub enum StackWatch {
Raster(RasterWatch),
Vector(VectorWatch),
}
impl StackWatch {
fn poll(&mut self) -> Option<String> {
match self {
Self::Raster(watch) => watch.poll(),
Self::Vector(watch) => watch.poll(),
}
}
}
impl ProviderWatch {
pub fn install_raster(&mut self, watch: RasterWatch) {
self.raster = watch;
}
pub fn install_stack(&mut self, layer: LayerId, watch: StackWatch) {
match self.stack.iter_mut().find(|(id, _)| *id == layer) {
Some(slot) => slot.1 = watch,
None => self.stack.push((layer, watch)),
}
}
pub fn remove_stack(&mut self, layers: &[LayerId]) {
self.stack.retain(|(id, _)| !layers.contains(id));
}
pub fn poll(&mut self) -> Option<String> {
if let Some(message) = self.raster.poll() {
return Some(message);
}
self.stack.iter_mut().find_map(|(_, watch)| watch.poll())
}
}
#[derive(Default)]
pub struct RasterWatch {
cog: Option<Arc<CogTileProvider>>,
archive: Option<Arc<ArchiveTileProvider>>,
basemap: Option<Arc<XyzTileProvider>>,
cog_reported: bool,
archive_reported: bool,
archive_tiles_reported: bool,
basemap_reported: bool,
}
impl RasterWatch {
pub fn basemap(basemap: Arc<XyzTileProvider>) -> Self {
Self {
basemap: Some(basemap),
..Self::default()
}
}
pub fn cog(cog: Arc<CogTileProvider>, basemap: Option<Arc<XyzTileProvider>>) -> Self {
Self {
cog: Some(cog),
basemap,
..Self::default()
}
}
pub fn archive(
archive: Arc<ArchiveTileProvider>,
basemap: Option<Arc<XyzTileProvider>>,
) -> Self {
Self {
archive: Some(archive),
basemap,
..Self::default()
}
}
pub fn into_basemap(self) -> Option<Arc<XyzTileProvider>> {
self.basemap
}
fn poll(&mut self) -> Option<String> {
if !self.cog_reported
&& let Some(message) = self.cog.as_ref().and_then(|cog| cog.failure())
{
self.cog_reported = true;
return Some(format!("The COG could not be read: {message}"));
}
if !self.archive_reported
&& let Some(message) = self.archive.as_ref().and_then(|archive| archive.failure())
{
self.archive_reported = true;
self.archive_tiles_reported = true;
return Some(format!("The tile archive could not be read: {message}"));
}
if !self.archive_tiles_reported
&& let Some(archive) = self.archive.as_ref()
{
let failed = archive.stats().failed;
if failed >= DEAD_SOURCE_FAILURES {
self.archive_tiles_reported = true;
return Some(format!(
"{failed} tiles could not be read from the archive. If it changed on the \
server, remove and re-add the layer.",
));
}
}
if !self.basemap_reported
&& let Some(message) = self
.basemap
.as_ref()
.and_then(|basemap| basemap_failure(basemap))
{
self.basemap_reported = true;
return Some(message);
}
None
}
}
#[derive(Default)]
pub struct VectorWatch {
provider: Option<Arc<VectorTileProvider>>,
archive: Option<ArchiveTileTransport>,
archive_reported: bool,
tiles_reported: bool,
}
impl VectorWatch {
pub fn new(provider: Arc<VectorTileProvider>, archive: Option<ArchiveTileTransport>) -> Self {
Self {
provider: Some(provider),
archive,
..Self::default()
}
}
fn poll(&mut self) -> Option<String> {
if !self.archive_reported
&& let Some(message) = self.archive.as_ref().and_then(|archive| archive.failure())
{
self.archive_reported = true;
self.tiles_reported = true;
return Some(format!(
"The vector tile archive could not be read: {message}",
));
}
if !self.tiles_reported
&& let Some(message) = self
.provider
.as_ref()
.and_then(|provider| vector_failure(provider))
{
self.tiles_reported = true;
return Some(message);
}
None
}
}
fn basemap_failure(provider: &XyzTileProvider) -> Option<String> {
let health = provider.health();
if provider.stats().ready > 0 || health.total_failures < DEAD_SOURCE_FAILURES as u64 {
return None;
}
let reason = health
.last_error
.unwrap_or_else(|| "no reason was recorded".to_owned());
Some(format!(
"No basemap tile has loaded ({} failed): {reason}",
health.total_failures,
))
}
fn vector_failure(provider: &VectorTileProvider) -> Option<String> {
let health = provider.health();
if provider.stats().ready > 0
|| provider.decoded_len() > 0
|| health.total_failures < DEAD_SOURCE_FAILURES as u64
{
return None;
}
let reason = health
.last_error
.unwrap_or_else(|| "no reason was recorded".to_owned());
Some(format!(
"No vector tile has loaded ({} failed): {reason}",
health.total_failures,
))
}
pub struct SharedCog(pub Arc<CogTileProvider>);
impl TileProvider for SharedCog {
fn tile(&self, tile: oxigis_render::TileId) -> Option<oxigis_render::DecodedTile> {
self.0.tile(tile)
}
}
pub struct SharedArchive(pub Arc<ArchiveTileProvider>);
impl TileProvider for SharedArchive {
fn tile(&self, tile: oxigis_render::TileId) -> Option<oxigis_render::DecodedTile> {
self.0.tile(tile)
}
}
pub struct SharedXyz(pub Arc<XyzTileProvider>);
impl TileProvider for SharedXyz {
fn tile(&self, tile: oxigis_render::TileId) -> Option<oxigis_render::DecodedTile> {
self.0.tile(tile)
}
}
pub struct SharedVector(pub Arc<VectorTileProvider>);
impl VectorTileSource for SharedVector {
fn begin_frame(&self, view: oxigis_render::MapView) -> bool {
self.0.begin_frame(view)
}
fn mesh(&self, tile: oxigis_render::TileId) -> Option<oxigis_render::VectorMesh> {
self.0.mesh(tile)
}
fn decoded(&self, tile: oxigis_render::TileId) -> Option<Arc<oxigis_render::VectorTile>> {
self.0.decoded(tile)
}
fn label_table(&self) -> &oxigis_render::LabelTable {
self.0.label_table()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxigis_core::{ArchiveFormat, ArchiveRef};
use oxigis_ui::{
ArchiveLayerConfig, MemoryRangeTransport, TileError, TileSink, TileTransport,
VectorTileConfig,
};
#[test]
fn an_empty_watch_reports_nothing() {
assert_eq!(ProviderWatch::default().poll(), None);
}
#[test]
fn an_archive_that_fails_after_it_was_built_is_reported_once() {
let transport = MemoryRangeTransport::new(vec![0x7f; 512]);
let provider = ArchiveTileProvider::pmtiles(
"memory://not-an-archive".to_owned(),
&egui::Context::default(),
Box::new(transport),
)
.expect("the provider builds before anything is read");
let provider = Arc::new(provider);
let tile = oxigis_render::TileId::new(0, 0, 0).expect("0/0/0");
for _ in 0..8 {
if provider.tile(tile).is_some() || provider.failure().is_some() {
break;
}
}
assert!(
provider.failure().is_some(),
"512 bytes of filler are not an archive"
);
let mut watch = ProviderWatch::default();
watch.install_raster(RasterWatch::archive(provider, None));
let message = watch.poll().expect("the refusal must reach the shell");
assert!(
message.starts_with("The tile archive could not be read:"),
"{message}"
);
assert_eq!(watch.poll(), None);
}
#[test]
fn a_vector_archive_that_never_opens_is_reported_once() {
let archive = ArchiveLayerConfig::new(
ArchiveRef::Path {
path: "memory://not-an-archive".to_owned(),
},
ArchiveFormat::PmTiles,
);
let transport = ArchiveTileTransport::pmtiles(
archive.location().to_owned(),
Box::new(MemoryRangeTransport::new(vec![0x7f; 512])),
);
let config = VectorTileConfig {
archive: Some(archive),
..VectorTileConfig::new("memory://not-an-archive")
};
let provider = VectorTileProvider::new(
&config,
&egui::Context::default(),
Box::new(transport.clone()),
)
.expect("the provider builds before anything is read");
for x in 0..8 {
let tile = oxigis_render::TileId::new(4, x, 0).expect("a zoom-4 tile");
let _mesh = provider.mesh(tile);
}
assert!(
transport.failure().is_some(),
"512 bytes of filler are not an archive"
);
let provider = Arc::new(provider);
let mut watch = ProviderWatch::default();
let layer = LayerId::new();
watch.install_stack(
layer,
StackWatch::Vector(VectorWatch::new(
Arc::clone(&provider),
Some(transport.clone()),
)),
);
let message = watch.poll().expect("the refusal must reach the shell");
assert!(
message.starts_with("The vector tile archive could not be read:"),
"{message}"
);
assert!(provider.health().total_failures >= DEAD_SOURCE_FAILURES as u64);
assert_eq!(watch.poll(), None);
watch.remove_stack(&[layer]);
assert_eq!(watch.poll(), None);
}
struct DeadTransport;
impl TileTransport for DeadTransport {
fn request(&self, tile: oxigis_render::TileId, _url: String, sink: TileSink) {
sink.deliver(tile, Err(TileError::permanent("HTTP 403 Forbidden")));
}
}
#[test]
fn a_vector_source_that_refuses_every_tile_is_reported_once() {
let provider = VectorTileProvider::new(
&VectorTileConfig::new("https://tiles.invalid/{z}/{x}/{y}.pbf"),
&egui::Context::default(),
Box::new(DeadTransport),
)
.expect("the template is well formed");
for x in 0..=DEAD_SOURCE_FAILURES as u32 {
let tile = oxigis_render::TileId::new(4, x, 0).expect("a zoom-4 tile");
let _mesh = provider.mesh(tile);
}
assert!(provider.health().total_failures >= DEAD_SOURCE_FAILURES as u64);
let mut watch = ProviderWatch::default();
watch.install_stack(
LayerId::new(),
StackWatch::Vector(VectorWatch::new(Arc::new(provider), None)),
);
let message = watch.poll().expect("a source refusing everything reports");
assert!(
message.starts_with("No vector tile has loaded"),
"{message}"
);
assert!(message.contains("403"), "{message}");
assert_eq!(watch.poll(), None);
}
#[test]
fn a_source_under_the_failure_threshold_is_not_called_broken() {
let provider = VectorTileProvider::new(
&VectorTileConfig::new("https://tiles.invalid/{z}/{x}/{y}.pbf"),
&egui::Context::default(),
Box::new(DeadTransport),
)
.expect("the template is well formed");
let tile = oxigis_render::TileId::new(4, 0, 0).expect("a zoom-4 tile");
let _mesh = provider.mesh(tile);
assert_eq!(vector_failure(&provider), None);
}
#[test]
fn installing_a_basemap_keeps_the_stack_watches() {
let provider = VectorTileProvider::new(
&VectorTileConfig::new("https://tiles.invalid/{z}/{x}/{y}.pbf"),
&egui::Context::default(),
Box::new(DeadTransport),
)
.expect("the template is well formed");
for x in 0..=DEAD_SOURCE_FAILURES as u32 {
let tile = oxigis_render::TileId::new(4, x, 0).expect("a zoom-4 tile");
let _mesh = provider.mesh(tile);
}
let mut watch = ProviderWatch::default();
watch.install_stack(
LayerId::new(),
StackWatch::Vector(VectorWatch::new(Arc::new(provider), None)),
);
watch.install_raster(RasterWatch::default());
let message = watch.poll().expect("the vector refusal survives");
assert!(
message.starts_with("No vector tile has loaded"),
"{message}"
);
}
}