use serde_json::{Map, Value};
use crate::maplibre::filter;
use crate::maplibre::layers::fill::{resolve_number, resolve_paint_color};
use crate::maplibre::layers::paint_of;
use crate::maplibre::sources::{features_node, resolve_layer_source, Sources};
use crate::maplibre::{Report, ZoomRange};
pub(crate) fn convert_line(
id: &str,
layer: &Map<String, Value>,
nodes: &mut Map<String, Value>,
outputs: &mut Vec<String>,
zoom_range: ZoomRange,
sources: &Sources,
report: &mut Report,
) {
let Some((source, source_layer)) = resolve_layer_source(id, layer, sources, report) else {
return;
};
let (min_zoom, max_zoom) = zoom_range;
let base_filter_expr = filter::layer_filter_expr(layer, report, id);
let paint = paint_of(layer);
let (width_const, width_expr) = resolve_number(paint.get("line-width"));
let width = width_const.unwrap_or(1.0).max(0.1);
let (gap_const, gap_expr) = resolve_number(paint.get("line-gap-width"));
if let Some(pattern) = paint.get("line-pattern") {
let (opacity, _) = resolve_number(paint.get("line-opacity"));
convert_line_pattern(
id,
pattern,
&source,
&source_layer,
base_filter_expr,
zoom_range,
width,
opacity,
sources,
nodes,
outputs,
report,
);
return;
}
let (color_hex, color_expr) = resolve_paint_color(paint.get("line-color"));
let hex = color_hex.unwrap_or_else(|| "#000000".to_string());
let (opacity, opacity_expr) = resolve_number(paint.get("line-opacity"));
let layout = layer.get("layout").and_then(Value::as_object);
let cap = layout
.and_then(|l| l.get("line-cap"))
.and_then(Value::as_str)
.unwrap_or("butt");
let join = layout
.and_then(|l| l.get("line-join"))
.and_then(Value::as_str)
.unwrap_or("miter");
let feat_id = format!("{id}__feat");
let stroke_id = format!("{id}__stroke");
nodes.insert(
feat_id.clone(),
features_node(&source, &source_layer, base_filter_expr, min_zoom, max_zoom),
);
let mut spec = serde_json::json!({
"op": "stroke",
"features": format!("@{feat_id}"),
"color": hex,
"width-px": width,
"cap": cap,
"join": join,
});
if let Some(expr) = color_expr {
spec["color-expr"] = expr;
}
if let Some(expr) = width_expr {
spec["width-expr"] = expr;
}
if let Some(g) = gap_const {
spec["gap-width-px"] = Value::from(g);
}
if let Some(expr) = gap_expr {
spec["gap-width-expr"] = expr;
}
if let Some(a) = opacity {
spec["opacity"] = Value::from(a);
}
if let Some(expr) = opacity_expr {
spec["opacity-expr"] = expr;
}
if let Some(arr) = paint.get("line-dasharray").and_then(Value::as_array) {
let dash: Vec<Value> = arr
.iter()
.filter_map(Value::as_f64)
.map(|d| Value::from(d * width))
.collect();
if !dash.is_empty() {
spec["dasharray"] = Value::Array(dash);
}
}
nodes.insert(stroke_id.clone(), spec);
outputs.push(stroke_id);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn convert_line_pattern(
id: &str,
pattern: &Value,
source: &str,
source_layer: &str,
base_filter_expr: Option<Value>,
zoom_range: ZoomRange,
width: f64,
opacity: Option<f64>,
sources: &Sources,
nodes: &mut Map<String, Value>,
outputs: &mut Vec<String>,
report: &mut Report,
) {
let (min_zoom, max_zoom) = zoom_range;
let Some(name) = pattern.as_str() else {
report.warn(format!(
"layer `{id}`: data-driven `line-pattern` not supported — skipped"
));
return;
};
let Some((sprite_src, icon_name)) = sources.resolve_icon(name) else {
report.warn(format!(
"layer `{id}`: line-pattern `{name}` needs a `sprite`, but the style declares none — skipped"
));
return;
};
let feat_id = format!("{id}__feat");
nodes.insert(
feat_id.clone(),
features_node(source, source_layer, base_filter_expr, min_zoom, max_zoom),
);
let icon_id = format!("{id}__icon");
nodes.insert(
icon_id.clone(),
serde_json::json!({ "op": "icon", "sprite": format!("@{sprite_src}"), "name": icon_name }),
);
let out_id = format!("{id}__linepat");
let mut spec = serde_json::json!({
"op": "line-stamp", "features": format!("@{feat_id}"),
"image": format!("@{icon_id}"), "width-px": width
});
if let Some(a) = opacity {
spec["opacity"] = Value::from(a);
}
nodes.insert(out_id.clone(), spec);
outputs.push(out_id);
}