use std::collections::HashMap;
use std::fmt::Debug;
#[cfg(all(feature = "rendering", target_os = "linux"))]
use std::num::NonZeroUsize;
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use dashmap::{DashMap, Entry};
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub use maplibre_native::Image as StaticImage;
#[cfg(all(feature = "rendering", target_os = "linux"))]
use maplibre_native::Image;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use tracing::{info, warn};
#[cfg(all(feature = "rendering", target_os = "linux"))]
mod error;
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub use error::StyleError;
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub mod render_pool;
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub use render_pool::RenderParams;
#[cfg(all(feature = "rendering", target_os = "linux"))]
use render_pool::RenderPools;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(
feature = "unstable-schemas",
derive(schemars::JsonSchema, utoipa::ToSchema)
)]
pub enum StyleKind {
Vector,
Raster,
Hybrid,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
feature = "unstable-schemas",
derive(schemars::JsonSchema, utoipa::ToSchema)
)]
pub struct CatalogStyleEntry {
#[cfg_attr(feature = "unstable-schemas", schemars(with = "String"))]
#[cfg_attr(feature = "unstable-schemas", schema(value_type = String))]
pub path: PathBuf,
#[serde(rename = "type")]
pub kind: Option<StyleKind>,
pub version_hash: Option<String>,
pub layer_count: Option<u32>,
pub colors: Option<Vec<String>>,
pub last_modified_at: Option<DateTime<Utc>>,
}
pub type StyleCatalog = HashMap<String, CatalogStyleEntry>;
#[derive(Debug, Clone, Default)]
pub struct StyleSources {
sources: DashMap<String, StyleSource>,
#[cfg(all(feature = "rendering", target_os = "linux"))]
pools: Option<RenderPools>,
}
#[derive(Clone, Debug)]
pub struct StyleSource {
path: PathBuf,
}
impl StyleSources {
#[must_use]
pub fn style_json_path(&self, style_id: &str) -> Option<PathBuf> {
let style_id = style_id.trim_end_matches(".json").trim();
let item = self.sources.get(style_id)?;
Some(item.path.clone())
}
#[must_use]
pub fn get_catalog(&self) -> StyleCatalog {
let mut entries = StyleCatalog::new();
for source in &self.sources {
entries.insert(
source.key().clone(),
CatalogStyleEntry {
path: source.path.clone(),
kind: None,
version_hash: None,
layer_count: None,
colors: None,
last_modified_at: None,
},
);
}
entries
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
#[must_use]
pub fn is_rendering_enabled(&self) -> bool {
self.pools.is_some()
}
pub fn add_style(&mut self, id: String, path: PathBuf) {
debug_assert!(path.is_file());
debug_assert!(!id.is_empty());
match self.sources.entry(id) {
Entry::Occupied(v) => {
warn!(
source.id = %v.key(),
style.path.kept = %v.get().path.display(),
style.path.dropped = %path.display(),
"Ignoring duplicate style source: already configured for another path"
);
}
Entry::Vacant(v) => {
info!(
source.id = %v.key(),
style.path = %path.display(),
"Configured style source"
);
v.insert(StyleSource { path });
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.sources.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sources.is_empty()
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub async fn render(&self, path: PathBuf, z: u8, x: u32, y: u32) -> Result<Image, StyleError> {
self.pools
.as_ref()
.ok_or(StyleError::RenderingIsDisabled)?
.render_tile(path, z, x, y)
.await
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub async fn render_static(&self, params: RenderParams) -> Result<Image, StyleError> {
self.pools
.as_ref()
.ok_or(StyleError::RenderingIsDisabled)?
.render_static(params)
.await
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub fn enable_rendering(
&mut self,
workers: Option<NonZeroUsize>,
) -> Result<(), std::io::Error> {
self.pools = Some(RenderPools::new(workers)?);
Ok(())
}
#[cfg(all(feature = "rendering", target_os = "linux"))]
pub fn disable_rendering(&mut self) {
self.pools = None;
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn test_style_external() {
let style_dir = Path::new("../tests/fixtures/styles/");
let mut styles = StyleSources::default();
styles.add_style(
"maplibre_demo".to_string(),
style_dir.join("maplibre_demo.json"),
);
styles.add_style(
"maptiler_basic".to_string(),
style_dir.join("src2").join("maptiler_basic.json"),
);
styles.add_style(
"osm-liberty-lite".to_string(),
style_dir.join("src2").join("osm-liberty-lite.json"),
);
assert_eq!(styles.sources.len(), 3);
let catalog = styles.get_catalog();
insta::with_settings!({sort_maps => true}, {
insta::assert_json_snapshot!(catalog, @r#"
{
"maplibre_demo": {
"path": "../tests/fixtures/styles/maplibre_demo.json"
},
"maptiler_basic": {
"path": "../tests/fixtures/styles/src2/maptiler_basic.json"
},
"osm-liberty-lite": {
"path": "../tests/fixtures/styles/src2/osm-liberty-lite.json"
}
}
"#);
});
assert_eq!(styles.style_json_path("NON_EXISTENT"), None);
assert_eq!(
styles.style_json_path("maplibre_demo.json"),
Some(style_dir.join("maplibre_demo.json"))
);
assert_eq!(styles.style_json_path("src2"), None);
let src2_dir = style_dir.join("src2");
assert_eq!(
styles.style_json_path("maptiler_basic"),
Some(src2_dir.join("maptiler_basic.json"))
);
assert_eq!(
styles.style_json_path("maptiler_basic.json"),
Some(src2_dir.join("maptiler_basic.json"))
);
assert_eq!(
styles.style_json_path("osm-liberty-lite.json"),
Some(src2_dir.join("osm-liberty-lite.json"))
);
}
}