use crate::aes::Aesthetic;
use crate::coord::Coord;
use crate::data::{DataFrame, Value};
use crate::position::identity::PositionIdentity;
use crate::position::Position;
use crate::render::backend::{DrawBackend, LineStyle, Linetype, PointShape, PointStyle, RectStyle};
use crate::render::RenderError;
use crate::scale::ScaleSet;
use crate::spatial::{parse_wkt, Geometry, SfProjection};
use crate::stat::Stat;
use crate::theme::Theme;
use super::{Geom, GeomParams};
fn value_str(v: &Value) -> String {
match v {
Value::Str(s) => s.clone(),
Value::Float(f) => {
let r = (f * 1000.0).round() / 1000.0;
format!("{r}")
}
Value::Integer(i) => format!("{i}"),
Value::Bool(b) => format!("{b}"),
_ => String::new(),
}
}
fn projected_bounds(g: &Geometry, proj: SfProjection) -> Option<(f64, f64, f64, f64)> {
let mut b: Option<(f64, f64, f64, f64)> = None;
g.for_each_coord(&mut |c| {
let [x, y] = proj.project(c);
b = Some(match b {
None => (x, y, x, y),
Some((mnx, mny, mxx, mxy)) => (mnx.min(x), mny.min(y), mxx.max(x), mxy.max(y)),
});
});
b
}
#[derive(Default)]
pub struct StatSf {
pub projection: SfProjection,
}
impl Stat for StatSf {
fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
let mut out = data.clone();
let Some(geom) = data.column("geometry") else {
return out;
};
let n = geom.len();
let (mut xmin, mut xmax) = (Vec::with_capacity(n), Vec::with_capacity(n));
let (mut ymin, mut ymax) = (Vec::with_capacity(n), Vec::with_capacity(n));
for v in geom.iter() {
let bounds = match v {
Value::Str(s) => parse_wkt(s).and_then(|g| projected_bounds(&g, self.projection)),
_ => None,
};
match bounds {
Some((mnx, mny, mxx, mxy)) => {
xmin.push(Value::Float(mnx));
xmax.push(Value::Float(mxx));
ymin.push(Value::Float(mny));
ymax.push(Value::Float(mxy));
}
None => {
xmin.push(Value::Na);
xmax.push(Value::Na);
ymin.push(Value::Na);
ymax.push(Value::Na);
}
}
}
out.add_column("xmin".to_string(), xmin);
out.add_column("xmax".to_string(), xmax);
out.add_column("ymin".to_string(), ymin);
out.add_column("ymax".to_string(), ymax);
out
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![]
}
fn name(&self) -> &str {
"sf"
}
}
pub struct GeomSf {
pub fill: (u8, u8, u8),
pub color: (u8, u8, u8),
pub alpha: f64,
pub line_width: f64,
pub point_size: f64,
pub projection: SfProjection,
pub simplify_px: f64,
}
impl Default for GeomSf {
fn default() -> Self {
GeomSf {
fill: (97, 156, 255),
color: (50, 50, 50),
alpha: 0.85,
line_width: 0.5,
point_size: 3.0,
projection: SfProjection::default(),
simplify_px: 0.5,
}
}
}
impl GeomSf {
pub fn project(mut self, projection: SfProjection) -> Self {
self.projection = projection;
self
}
pub fn simplify(mut self, tolerance_px: f64) -> Self {
self.simplify_px = tolerance_px.max(0.0);
self
}
fn simplified(&self, pts: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
if self.simplify_px <= 0.0 || pts.len() < 3 {
return pts;
}
simplify_path(&pts, self.simplify_px)
}
fn draw_geometry(
&self,
g: &Geometry,
project: &impl Fn([f64; 2]) -> (f64, f64),
fill: (u8, u8, u8),
color: (u8, u8, u8),
backend: &mut dyn DrawBackend,
) -> Result<(), RenderError> {
match g {
Geometry::Empty => {}
Geometry::Point(c) => self.draw_point(*c, project, fill, backend)?,
Geometry::MultiPoint(cs) => {
for c in cs {
self.draw_point(*c, project, fill, backend)?;
}
}
Geometry::LineString(cs) => self.draw_path(cs, project, fill, backend)?,
Geometry::MultiLineString(lines) => {
for l in lines {
self.draw_path(l, project, fill, backend)?;
}
}
Geometry::Polygon(rings) => self.draw_rings(rings, project, fill, color, backend)?,
Geometry::MultiPolygon(polys) => {
for p in polys {
self.draw_rings(p, project, fill, color, backend)?;
}
}
Geometry::Collection(parts) => {
for part in parts {
self.draw_geometry(part, project, fill, color, backend)?;
}
}
}
Ok(())
}
fn draw_point(
&self,
c: [f64; 2],
project: &impl Fn([f64; 2]) -> (f64, f64),
color: (u8, u8, u8),
backend: &mut dyn DrawBackend,
) -> Result<(), RenderError> {
backend.draw_circle(
project(c),
self.point_size,
&PointStyle {
color,
alpha: self.alpha,
filled: true,
shape: PointShape::Circle,
},
)
}
fn draw_path(
&self,
cs: &[[f64; 2]],
project: &impl Fn([f64; 2]) -> (f64, f64),
color: (u8, u8, u8),
backend: &mut dyn DrawBackend,
) -> Result<(), RenderError> {
if cs.len() < 2 {
return Ok(());
}
let pts: Vec<(f64, f64)> = cs.iter().map(|c| project(*c)).collect();
let pts = self.simplified(pts);
backend.draw_line(
&pts,
&LineStyle {
color,
alpha: self.alpha,
width: self.line_width.max(1.0),
linetype: Linetype::Solid,
..Default::default()
},
)
}
fn draw_rings(
&self,
rings: &[Vec<[f64; 2]>],
project: &impl Fn([f64; 2]) -> (f64, f64),
fill: (u8, u8, u8),
color: (u8, u8, u8),
backend: &mut dyn DrawBackend,
) -> Result<(), RenderError> {
let mut screen_rings: Vec<Vec<(f64, f64)>> = Vec::with_capacity(rings.len());
for ring in rings {
if ring.len() < 3 {
continue;
}
let pts: Vec<(f64, f64)> = ring.iter().map(|c| project(*c)).collect();
let simplified = self.simplified(pts.clone());
screen_rings.push(if simplified.len() >= 3 {
simplified
} else {
pts
});
}
if screen_rings.is_empty() {
return Ok(());
}
backend.draw_polygon_rings(
&screen_rings,
&RectStyle {
fill: Some(fill),
stroke: (self.line_width > 0.0).then_some(color),
stroke_width: self.line_width,
alpha: self.alpha,
clip: true,
corner_radius: 0.0,
},
)
}
}
fn simplify_path(pts: &[(f64, f64)], tol: f64) -> Vec<(f64, f64)> {
let finite: Vec<(f64, f64)> = pts
.iter()
.copied()
.filter(|(x, y)| x.is_finite() && y.is_finite())
.collect();
if finite.len() < 3 {
return finite;
}
let last = finite.len() - 1;
let mut keep = vec![false; finite.len()];
keep[0] = true;
keep[last] = true;
let mut stack = vec![(0usize, last)];
while let Some((a, b)) = stack.pop() {
if b <= a + 1 {
continue;
}
let (mut far, mut dmax) = (a, -1.0);
for (i, p) in finite.iter().enumerate().take(b).skip(a + 1) {
let d = seg_dist(*p, finite[a], finite[b]);
if d > dmax {
dmax = d;
far = i;
}
}
if dmax > tol {
keep[far] = true;
stack.push((a, far));
stack.push((far, b));
}
}
finite
.into_iter()
.zip(keep)
.filter_map(|(p, k)| k.then_some(p))
.collect()
}
fn seg_dist(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len2 = dx * dx + dy * dy;
let t = if len2 > 0.0 {
(((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len2).clamp(0.0, 1.0)
} else {
0.0
};
let (ex, ey) = (a.0 + t * dx - p.0, a.1 + t * dy - p.1);
(ex * ex + ey * ey).sqrt()
}
impl Geom for GeomSf {
fn draw(
&self,
data: &DataFrame,
coord: &dyn Coord,
scales: &ScaleSet,
_theme: &Theme,
backend: &mut dyn DrawBackend,
) -> Result<(), RenderError> {
let geom_col = data
.column("geometry")
.ok_or(RenderError::MissingAesthetic("geometry".into()))?;
let fill_col = data.column("fill");
let color_col = data.column("color");
let label_col = data.column("label");
let plot_area = backend.plot_area();
let x_scale = scales.get(&Aesthetic::X);
let y_scale = scales.get(&Aesthetic::Y);
let project = |c: [f64; 2]| {
let [x, y] = self.projection.project(c);
let vx = Value::Float(x);
let vy = Value::Float(y);
let nx = x_scale.map(|s| s.map(&vx)).unwrap_or(0.0);
let ny = y_scale.map(|s| s.map(&vy)).unwrap_or(0.0);
coord.transform((nx, ny), &plot_area)
};
for i in 0..data.nrows() {
let g = match &geom_col[i] {
Value::Str(s) => parse_wkt(s),
_ => None,
};
let Some(g) = g else { continue };
let fill = fill_col
.and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[i]))
.unwrap_or(self.fill);
let color = color_col
.and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[i]))
.unwrap_or(self.color);
let label = label_col
.map(|c| value_str(&c[i]))
.filter(|s| !s.is_empty());
let fill_val = fill_col.map(|c| value_str(&c[i])).filter(|s| !s.is_empty());
backend.set_tooltip(match (label, fill_val) {
(Some(l), Some(f)) => Some(format!("{l}: {f}")),
(Some(l), None) => Some(l),
(None, Some(f)) => Some(f),
(None, None) => None,
});
super::set_row_extras(backend, data, i);
self.draw_geometry(&g, &project, fill, color, backend)?;
}
backend.set_tooltip(None);
super::clear_mark(backend);
super::clear_row_extras(backend);
Ok(())
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![]
}
fn default_stat(&self) -> Box<dyn Stat> {
Box::new(StatSf {
projection: self.projection,
})
}
fn default_position(&self) -> Box<dyn Position> {
Box::new(PositionIdentity)
}
fn default_params(&self) -> GeomParams {
GeomParams::default()
}
fn name(&self) -> &str {
"sf"
}
fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
self.color = ink;
}
}
fn consumes_tooltips(&self) -> bool {
true
}
fn set_series_color(&mut self, color: (u8, u8, u8)) {
self.fill = color;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simplify_path_keeps_endpoints_and_drops_collinear_points() {
let line: Vec<(f64, f64)> = (0..=20)
.map(|i| (i as f64, if i % 2 == 0 { 0.0 } else { 0.1 }))
.collect();
let simplified = simplify_path(&line, 0.5);
assert_eq!(simplified, vec![(0.0, 0.0), (20.0, 0.0)]);
}
#[test]
fn simplify_path_keeps_a_real_bump() {
let bump = [(0.0, 0.0), (5.0, 3.0), (10.0, 0.0)];
assert_eq!(simplify_path(&bump, 0.5).len(), 3);
}
#[test]
fn simplify_path_zero_tolerance_is_a_no_op() {
let pts = [(0.0, 0.0), (1.0, 0.01), (2.0, 0.0), (3.0, 10.0)];
let simplified = simplify_path(&pts, 1e-9);
assert_eq!(simplified.len(), 4);
}
#[test]
fn simplify_path_drops_non_finite_points() {
let pts = [(0.0, 0.0), (f64::NAN, 1.0), (1.0, 0.0), (2.0, 0.0)];
let simplified = simplify_path(&pts, 0.5);
assert!(simplified
.iter()
.all(|(x, y)| x.is_finite() && y.is_finite()));
assert_eq!(simplified.len(), 2, "endpoints of the finite run");
}
#[test]
fn default_simplify_px_is_half_a_pixel() {
assert_eq!(GeomSf::default().simplify_px, 0.5);
}
#[test]
fn simplify_builder_clamps_negative_to_zero() {
let g = GeomSf::default().simplify(-1.0);
assert_eq!(g.simplify_px, 0.0);
}
}