snapr/drawing/geometry/
line.rs

1//! Contains [`Drawable`] implementations and [`Styles`](Style) for [`geo::Line`], and [`geo::LineString`] primitives.
2
3use std::fmt;
4
5use geo::MapCoords;
6use tiny_skia::{Color, Paint, PathBuilder, Pixmap, Shader, Stroke, Transform};
7
8use crate::drawing::{
9    style::{ColorOptions, Effect, Styleable, Styled},
10    Context, Drawable,
11};
12
13use super::{macros::impl_styled_geo, point::PointStyle};
14
15macro_rules! impl_line_style {
16    ($style: ident, $line: ident) => {
17        #[derive(Clone)]
18        #[doc = concat!("A style that can be applied to the [`geo::", stringify!($line), "`] primitive.")]
19        pub struct $style<'a> {
20            pub color_options: ColorOptions,
21            pub point_style: PointStyle<'a>,
22            pub width: f32,
23            pub effect: Option<Effect<'a, geo::$line<f64>, Self>>,
24        }
25
26        impl<'a> fmt::Debug for $style<'a> {
27            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28                f.debug_struct(stringify!($style))
29                    .field("color_options", &self.color_options)
30                    .field("point_style", &self.point_style)
31                    .field("width", &self.width)
32                    .finish()
33            }
34        }
35
36        impl<'a> Default for $style<'a> {
37            fn default() -> Self {
38                Self {
39                    color_options: ColorOptions {
40                        foreground: Color::from_rgba8(196, 196, 196, 255),
41                        border: Some(4.0),
42                        ..ColorOptions::default()
43                    },
44                    point_style: PointStyle::default(),
45                    width: 3.0,
46                    effect: None,
47                }
48            }
49        }
50    };
51}
52
53impl_line_style!(LineStyle, Line);
54impl_line_style!(LineStringStyle, LineString);
55
56impl_styled_geo!(
57    Line,
58    LineStyle<'_>,
59    fn draw(&self, pixmap: &mut Pixmap, context: &Context) -> Result<(), crate::Error> {
60        let style = match &self.style.effect {
61            Some(effect) => {
62                &(effect
63                    .clone()
64                    .apply(self.style.clone(), self.inner, context))
65            }
66
67            None => &self.style,
68        };
69
70        let line = self
71            .inner
72            .map_coords(|coord| context.epsg_4326_to_pixel(&coord));
73
74        #[cfg(feature = "tracing")]
75        {
76            tracing::trace!(start = ?line.start, end = ?line.end, "rendering `Line` to `pixmap`");
77        }
78
79        let mut path_builder = PathBuilder::new();
80        path_builder.move_to(line.start.x as f32, line.start.y as f32);
81        path_builder.line_to(line.end.x as f32, line.end.y as f32);
82
83        let line = path_builder
84            .finish()
85            .ok_or(crate::Error::PathConstruction)?;
86
87        if let Some(border) = style.color_options.border {
88            pixmap.stroke_path(
89                &line,
90                &Paint {
91                    shader: Shader::SolidColor(style.color_options.background),
92                    anti_alias: style.color_options.anti_alias,
93                    ..Paint::default()
94                },
95                &Stroke {
96                    width: border,
97                    ..Stroke::default()
98                },
99                Transform::default(),
100                None,
101            );
102        }
103
104        pixmap.stroke_path(
105            &line,
106            &Paint {
107                shader: Shader::SolidColor(style.color_options.foreground),
108                anti_alias: style.color_options.anti_alias,
109                ..Paint::default()
110            },
111            &Stroke {
112                width: style.width,
113                ..Stroke::default()
114            },
115            Transform::default(),
116            None,
117        );
118
119        self.inner
120            .start_point()
121            .as_styled(style.point_style.clone())
122            .draw(
123                pixmap,
124                &Context {
125                    index: 0,
126                    ..context.clone()
127                },
128            )?;
129
130        self.inner
131            .end_point()
132            .as_styled(style.point_style.clone())
133            .draw(
134                pixmap,
135                &Context {
136                    index: 1,
137                    ..context.clone()
138                },
139            )?;
140
141        Ok(())
142    }
143);
144
145impl_styled_geo!(
146    LineString,
147    LineStringStyle<'_>,
148    fn draw(&self, pixmap: &mut Pixmap, context: &Context) -> Result<(), crate::Error> {
149        let style = match &self.style.effect {
150            Some(effect) => {
151                &(effect
152                    .clone()
153                    .apply(self.style.clone(), self.inner, context))
154            }
155
156            None => &self.style,
157        };
158
159        let mut path_builder = PathBuilder::new();
160
161        let line_string = self
162            .inner
163            .map_coords(|coord| context.epsg_4326_to_pixel(&coord));
164
165        #[cfg(feature = "tracing")]
166        {
167            tracing::trace!("rendering `LineString` to `pixmap`");
168        }
169
170        for (index, point) in line_string.points().enumerate() {
171            if index == 0 {
172                path_builder.move_to(point.x() as f32, point.y() as f32);
173            } else {
174                path_builder.line_to(point.x() as f32, point.y() as f32);
175            }
176        }
177
178        if let Some(lines) = path_builder.finish() {
179            if let Some(border) = style.color_options.border {
180                pixmap.stroke_path(
181                    &lines,
182                    &Paint {
183                        shader: Shader::SolidColor(style.color_options.background),
184                        anti_alias: style.color_options.anti_alias,
185                        ..Paint::default()
186                    },
187                    &Stroke {
188                        width: border,
189                        ..Stroke::default()
190                    },
191                    Transform::default(),
192                    None,
193                );
194            }
195
196            pixmap.stroke_path(
197                &lines,
198                &Paint {
199                    shader: Shader::SolidColor(style.color_options.foreground),
200                    anti_alias: style.color_options.anti_alias,
201                    ..Paint::default()
202                },
203                &Stroke {
204                    width: style.width,
205                    ..Stroke::default()
206                },
207                Transform::default(),
208                None,
209            );
210        }
211
212        self.inner
213            .points()
214            .enumerate()
215            .try_for_each(|(index, point)| {
216                let context = &Context {
217                    index,
218                    ..context.clone()
219                };
220
221                point
222                    .as_styled(style.point_style.clone())
223                    .draw(pixmap, context)
224            })?;
225
226        Ok(())
227    }
228);
229
230impl_styled_geo!(
231    MultiLineString,
232    LineStringStyle<'_>,
233    fn draw(&self, pixmap: &mut Pixmap, context: &Context) -> Result<(), crate::Error> {
234        self.inner
235            .iter()
236            .map(|line_string| line_string.as_styled(self.style.clone()))
237            .try_for_each(|line_string| line_string.draw(pixmap, context))
238    }
239);