use serde_json::{Map, Value};
use crate::maplibre::{ConvertError, Report};
#[derive(Default)]
pub(crate) struct Sources {
pub(crate) vector: Vec<String>,
pub(crate) geojson: Vec<String>,
pub(crate) sprites: Vec<String>,
}
impl Sources {
pub(crate) fn default_sprite(&self) -> Option<&str> {
self.sprites
.iter()
.find(|s| *s == "default")
.or_else(|| self.sprites.first())
.map(String::as_str)
}
pub(crate) fn resolve_icon<'a>(&self, name: &'a str) -> Option<(&str, &'a str)> {
if let Some((sheet, icon)) = name.split_once(':') {
if let Some(key) = self.sprites.iter().find(|s| *s == sheet) {
return Some((key, icon));
}
}
let key = self
.sprites
.iter()
.find(|s| *s == "default")
.or_else(|| self.sprites.first())?;
Some((key, name))
}
}
pub(crate) fn convert_sources(
style: &Map<String, Value>,
report: &mut Report,
) -> Result<(Map<String, Value>, Sources), ConvertError> {
let empty = Map::new();
let src = style
.get("sources")
.and_then(Value::as_object)
.unwrap_or(&empty);
let mut out = Map::new();
let mut sources = Sources::default();
for (name, decl) in src {
let Some(decl) = decl.as_object() else {
continue;
};
let ty = decl.get("type").and_then(Value::as_str).unwrap_or("");
let url = decl
.get("url")
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| {
decl.get("tiles")
.and_then(Value::as_array)
.and_then(|t| t.first())
.and_then(Value::as_str)
.map(str::to_string)
});
match ty {
"vector" => {
let Some(url) = url else {
report.warn(format!(
"source `{name}`: vector source has no url/tiles — skipped"
));
continue;
};
out.insert(
name.clone(),
serde_json::json!({ "type": "mvt", "url": url }),
);
sources.vector.push(name.clone());
}
"raster" => {
if let Some(url) = url {
out.insert(
name.clone(),
serde_json::json!({ "type": "raster", "url": url }),
);
} else {
report.warn(format!(
"source `{name}`: raster source has no url/tiles — skipped"
));
}
}
"geojson" => {
match decl.get("data") {
Some(Value::String(u)) => {
out.insert(
name.clone(),
serde_json::json!({ "type": "geojson", "url": u }),
);
sources.geojson.push(name.clone());
}
Some(data @ (Value::Object(_) | Value::Array(_))) => {
out.insert(
name.clone(),
serde_json::json!({ "type": "geojson", "data": data }),
);
sources.geojson.push(name.clone());
}
_ => report.warn(format!(
"source `{name}`: geojson source has no usable `data` — skipped"
)),
}
}
"raster-dem" => {
if let Some(url) = url {
let enc = decl
.get("encoding")
.and_then(Value::as_str)
.unwrap_or("mapbox");
let tile_size = decl.get("tileSize").and_then(Value::as_u64).unwrap_or(512);
let mut dem = serde_json::json!({
"type": "dem", "url": url, "encoding": enc,
"tile-size": tile_size, "neighbor-fetch": true
});
if let Some(mz) = decl.get("maxzoom").and_then(Value::as_u64) {
dem["max-zoom"] = Value::from(mz);
}
out.insert(name.clone(), dem);
} else {
report.warn(format!(
"source `{name}`: raster-dem has no url/tiles — skipped"
));
}
}
other => report.warn(format!(
"source `{name}`: type `{other}` not supported — skipped"
)),
}
}
let mut emit_sprite = |key: &str, base: &str, out: &mut Map<String, Value>| {
out.insert(
key.to_string(),
serde_json::json!({
"type": "sprite",
"image": format!("{base}.png"),
"index": format!("{base}.json"),
}),
);
sources.sprites.push(key.to_string());
};
match style.get("sprite") {
Some(Value::String(base)) => emit_sprite("default", base, &mut out),
Some(Value::Array(sheets)) => {
for sheet in sheets {
let id = sheet.get("id").and_then(Value::as_str);
let url = sheet.get("url").and_then(Value::as_str);
if let (Some(id), Some(url)) = (id, url) {
emit_sprite(id, url, &mut out);
} else {
report.warn("sprite sheet entry missing `id`/`url` — skipped".to_string());
}
}
}
_ => {}
}
if sources.vector.is_empty() && sources.geojson.is_empty() && out.is_empty() {
return Err(ConvertError::NoVectorSource);
}
Ok((out, sources))
}
pub(crate) fn resolve_layer_source(
id: &str,
layer: &Map<String, Value>,
sources: &Sources,
report: &mut Report,
) -> Option<(String, String)> {
let Some(s) = layer.get("source").and_then(Value::as_str) else {
report.warn(format!("layer `{id}`: no source — skipped"));
return None;
};
if sources.vector.iter().any(|v| v == s) {
let Some(sl) = layer.get("source-layer").and_then(Value::as_str) else {
report.warn(format!(
"layer `{id}`: vector layer without `source-layer` — skipped"
));
return None;
};
Some((s.to_string(), sl.to_string()))
} else if sources.geojson.iter().any(|g| g == s) {
Some((s.to_string(), s.to_string()))
} else {
report.warn(format!(
"layer `{id}`: source `{s}` is not a converted feature source — skipped"
));
None
}
}
pub(crate) fn features_node(
source: &str,
source_layer: &str,
filter_expr: Option<Value>,
min_zoom: Option<u8>,
max_zoom: Option<u8>,
) -> Value {
let mut m = Map::new();
m.insert("op".into(), Value::String("features".into()));
m.insert("source".into(), Value::String(source.to_string()));
m.insert("layer".into(), Value::String(source_layer.to_string()));
if let Some(expr) = filter_expr {
m.insert("filter-expr".into(), expr);
}
if let Some(z) = min_zoom {
m.insert("min-zoom".into(), Value::from(z));
}
if let Some(z) = max_zoom {
m.insert("max-zoom".into(), Value::from(z));
}
Value::Object(m)
}