aoer_plotty_rs/optimizer/
mod.rs

1use std::collections::HashMap;
2
3use geo::{prelude::EuclideanDistance, HasDimensions};
4use geo_types::{Coord as Coordinate, LineString, MultiLineString};
5use rstar::{PointDistance, RTree, RTreeObject, AABB};
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum OptimizationStrategy {
9    Greedy,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13/// Optimization strategy utility class.
14pub struct Optimizer {
15    max_keepdown: f64,
16    strategy: OptimizationStrategy,
17}
18
19impl Optimizer {
20    pub fn new(max_keepdown: f64, strategy: OptimizationStrategy) -> Optimizer {
21        Optimizer {
22            max_keepdown,
23            strategy,
24        }
25    }
26
27    pub fn build_rtree_from_hashmap(
28        &self,
29        hashmap: &HashMap<usize, LineString<f64>>,
30    ) -> RTree<LineRef> {
31        // Build out the RTree data
32        let mut linerefs: Vec<LineRef> = vec![];
33        for (i, line) in hashmap.iter() {
34            // Only add lines that are valid (2+ points)
35            if line.0.len() < 2 {
36                continue;
37            }
38            linerefs.push(LineRef::new(
39                *i,
40                line.0
41                    .first()
42                    .expect("We already bounds checked this?!")
43                    .clone(),
44                line.0
45                    .last()
46                    .expect("We already bounds checked this too!?")
47                    .clone(),
48                self.max_keepdown * 2., //.clone(),
49                true,
50            ));
51            linerefs.push(LineRef::new(
52                *i,
53                line.0
54                    .last()
55                    .expect("We already bounds checked this?!")
56                    .clone(),
57                line.0
58                    .first()
59                    .expect("We already bounds checked this too!?")
60                    .clone(),
61                self.max_keepdown * 2., //.clone(),
62                false,
63            ));
64        }
65
66        RTree::bulk_load(linerefs)
67    }
68
69    /// Merges lines who have endpoints at most pen_width*pi.sqrt() apart
70    pub fn merge(&self, mls: &MultiLineString<f64>) -> MultiLineString<f64> {
71        let mut lines_out = MultiLineString::new(vec![]); // Just one empty line in it.
72        let mut current_line: LineString<f64> = LineString::new(vec![]);
73        for source_line in mls.0.iter() {
74            if source_line.0.len() == 0 {
75                continue;
76            } // Skip blank/dot lines
77            let source_start = source_line.0.first().unwrap();
78            if current_line.0.len() == 0 {
79                current_line.0.append(&mut source_line.0.clone());
80            } else if current_line
81                .0
82                .last()
83                .unwrap()
84                .euclidean_distance(source_start)
85                <= self.max_keepdown
86            {
87                let mut tmpline = source_line.0.clone();
88                current_line.0.append(&mut tmpline);
89            } else {
90                lines_out.0.push(current_line.clone());
91                current_line = source_line.clone();
92            }
93        }
94        if current_line.0.len() > 0 {
95            lines_out.0.push(current_line)
96        }
97        lines_out
98    }
99
100    /// Optimizes lines by finding the nearest neighbor to each endpoint
101    /// using an rtree as a spatial index. Fast, but just greedy for now.
102    pub fn optimize(&self, mls: &MultiLineString<f64>) -> MultiLineString<f64> {
103        assert_eq!(self.strategy, OptimizationStrategy::Greedy);
104        let mut lines_out = MultiLineString::new(vec![]);
105        // if mls.0.len() == 0 {
106        if mls.0.len() == 0 {
107            return lines_out;
108        };
109        if mls.0.len() == 1 {
110            return mls.clone();
111        }
112        let mut line_count: usize = 0;
113        let mut lines_hash: HashMap<usize, LineString<f64>> =
114            HashMap::from_iter(mls.0.iter().map(|line| {
115                let pair = (line_count, line.clone());
116                line_count = line_count + 1;
117                pair
118            }));
119        let rtree = self.build_rtree_from_hashmap(&lines_hash);
120
121        while lines_hash.len() > 0 {
122            if let Some(tmpline) = lines_hash.remove(&0) {
123                if tmpline.is_empty() {
124                    continue;
125                }
126                if tmpline.0.len() > 1 {
127                    lines_out.0.push(tmpline); //.clone());
128                    break;
129                } else {
130                }
131            } else {
132                break;
133            }
134        }
135        while lines_hash.len() > 0 {
136            let line = lines_out
137                .0
138                .last()
139                .expect("Cannot pull line from list I just pushed to?!");
140            let last = match line.0.last() {
141                Some(point) => point,
142                None => {
143                    eprintln!("Failed to get last point for line: {:?}", &line);
144                    continue;
145                }
146            };
147            let mut found = false;
148            for neighbor_ref in rtree.nearest_neighbor_iter(&[last.x, last.y]) {
149                if let Some(mut neighbor_line) = lines_hash.remove(&neighbor_ref.line_id) {
150                    found = true;
151                    if neighbor_ref.fwd {
152                        lines_out.0.push(neighbor_line);
153                    } else {
154                        neighbor_line.0.reverse();
155                        lines_out.0.push(neighbor_line);
156                    }
157                    break;
158                }
159            }
160            if !found {
161                break;
162            }
163        } // We don't want to iterate the same item forever
164        let remaining_keys: Vec<usize> = lines_hash.keys().map(|k| k.clone()).collect();
165        for k in remaining_keys {
166            if let Some(line) = lines_hash.remove(&k) {
167                lines_out.0.push(line);
168            }
169        }
170
171        lines_out
172    }
173}
174
175/// A reference to a line which provides the line id, it's coordinates,
176/// and whether it's a forward or reverse traversal of the given line,
177/// as both are valid entries.
178#[derive(Clone, Debug, PartialEq)]
179pub struct LineRef {
180    line_id: usize,
181    start: Coordinate<f64>,
182    end: Coordinate<f64>,
183    pen_width: f64,
184    fwd: bool,
185}
186
187impl LineRef {
188    pub fn new(
189        line_id: usize,
190        start: Coordinate<f64>,
191        end: Coordinate<f64>,
192        pen_width: f64,
193        fwd: bool,
194    ) -> LineRef {
195        LineRef {
196            line_id,
197            start,
198            end,
199            pen_width,
200            fwd,
201        }
202    }
203}
204
205impl RTreeObject for LineRef {
206    type Envelope = AABB<[f64; 2]>;
207
208    fn envelope(&self) -> Self::Envelope {
209        AABB::from_corners(
210            [self.start.x - self.pen_width, self.start.y - self.pen_width],
211            [self.start.x + self.pen_width, self.start.y + self.pen_width],
212        )
213    }
214}
215
216impl PointDistance for LineRef {
217    fn distance_2(&self, point: &[f64; 2]) -> f64 {
218        let d_x = self.start.x - point[0];
219        let d_y = self.start.y - point[1];
220        let distance_to_start = (d_x * d_x + d_y * d_y).sqrt();
221        let distance_to_ring = distance_to_start - self.pen_width / 2.;
222        let distance_to_circle = f64::max(0.0, distance_to_ring);
223        // We must return the squared distance!
224        distance_to_circle * distance_to_circle
225    }
226
227    // This implementation is not required but more efficient since it
228    // omits the calculation of a square root
229    fn contains_point(&self, point: &[f64; 2]) -> bool {
230        let d_x = self.start.x - point[0];
231        let d_y = self.start.y - point[1];
232        let distance_to_start_2 = d_x * d_x + d_y * d_y;
233        let radius_2 = self.pen_width / 2. * self.pen_width / 2.;
234        distance_to_start_2 <= radius_2
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use std::sync::Arc;
241
242    use crate::prelude::LineHatch;
243
244    use super::*;
245    use geo_types::{coord, Polygon};
246    use rstar::RTree;
247    use wkt::ToWkt;
248
249    #[test]
250    fn test_optimizer_poly_nest() {
251        let mut ctx = crate::context::Context::new();
252        ctx.pen(1.)
253            .stroke("#000")
254            .fill("#000")
255            .hatch(0.)
256            .pen(0.5)
257            .pattern(Arc::new(Box::new(LineHatch {})));
258        let outer = LineString::new(vec![
259            coord! {x:0., y:10.},
260            coord! {x:14., y: 0.},
261            coord! {x:-14., y:0.},
262            coord! {x:0., y:10.},
263        ]);
264        let inner = vec![LineString::new(vec![
265            coord! {x:0., y:6.},
266            coord! {x:4.,y:4.},
267            coord! {x:-4., y:4.},
268            coord! {x:0., y:6.},
269        ])];
270        let test_poly = Polygon::<f64>::new(outer, inner);
271        ctx.geometry(&geo_types::Geometry::Polygon(test_poly));
272        for layer in ctx.to_layers() {
273            println!("LAYER:");
274            println!("STROKE: {}", layer.stroke_lines.to_wkt());
275            println!("FILL: {}", layer.fill_lines.to_wkt());
276        }
277    }
278
279    #[test]
280    fn test_optimizer_infill_premerged() {
281        let mut ctx = crate::context::Context::new();
282        ctx.pen(1.)
283            .stroke("#000")
284            .fill("#000")
285            .hatch(0.)
286            .pattern(Arc::new(Box::new(LineHatch {})))
287            .circle(0., 0., 10.);
288        let layers = ctx.to_layers();
289        for layer in layers {
290            let opt = Optimizer::new(layer.stroke_width * 1.5, OptimizationStrategy::Greedy);
291            let opt_stroke = opt.optimize(&layer.stroke_lines);
292            let merged_fill = opt.merge(&layer.fill_lines);
293            let opt_fill = opt.optimize(&merged_fill);
294            println!("LAYER:");
295            println!("STROKE: {}", opt_stroke.to_wkt());
296            println!("FILL: {}", opt_fill.to_wkt());
297        }
298    }
299
300    #[test]
301    fn test_optimizer_infill() {
302        let mut ctx = crate::context::Context::new();
303        ctx.pen(1.)
304            .stroke("#000")
305            .fill("#000")
306            .hatch(0.)
307            .pattern(Arc::new(Box::new(LineHatch {})))
308            .circle(0., 0., 10.);
309        let layers = ctx.to_layers();
310        for layer in layers {
311            let opt = Optimizer::new(layer.stroke_width, OptimizationStrategy::Greedy);
312            let opt_stroke = opt.optimize(&layer.stroke_lines);
313            let opt_fill = opt.optimize(&layer.fill_lines);
314            println!("LAYER:");
315            println!("STROKE: {}", opt_stroke.to_wkt());
316            println!("FILL: {}", opt_fill.to_wkt());
317        }
318    }
319
320    #[test]
321    fn test_lineref_basic() {
322        let line_0f = LineRef {
323            line_id: 0,
324            start: coord! {x:0., y:0.},
325            end: coord! {x:20., y:0.},
326
327            pen_width: 0.5,
328            fwd: true,
329        };
330        let line_0r = LineRef {
331            line_id: 1,
332            start: coord! {x:20., y:0.},
333            end: coord! {x:0., y:0.},
334            pen_width: 0.5,
335            fwd: false,
336        };
337        let line_1f = LineRef {
338            line_id: 2,
339            start: coord! {x:10., y:10.},
340            end: coord! {x:20., y:5.},
341            pen_width: 1.0,
342            fwd: true,
343        };
344        let line_1r = LineRef {
345            line_id: 3,
346            start: coord! {x:20., y:5.},
347            end: coord! {x:10., y:10.},
348            pen_width: 1.0,
349            fwd: false,
350        };
351        let mut tree = RTree::new();
352        tree.insert(line_0f.clone());
353        tree.insert(line_0r.clone());
354        tree.insert(line_1f.clone());
355        tree.insert(line_1r.clone());
356        let e = AABB::from_point([10., 10.]);
357        println!("e: {:?}", e);
358        println!(
359            "Closest neighbor to e is {:?}",
360            tree.nearest_neighbor(&[10., 10.])
361        );
362
363        for line in tree.nearest_neighbor_iter(&[10., 10.]) {
364            println!("Found line endpoint for line id: {}", line.line_id);
365        }
366    }
367
368    #[test]
369    fn test_optimizer_basic() {
370        let lines: MultiLineString<f64> = MultiLineString::new(vec![
371            LineString::new(vec![coord! {x: 0.0, y:20.0}, coord! {x:0.0, y:0.0}]),
372            LineString::new(vec![coord! {x: 0.0, y:0.0}, coord! {x:20.0, y:20.0}]),
373            LineString::new(vec![coord! {x: 20.0, y:20.5}, coord! {x:40.0, y:20.0}]),
374            LineString::new(vec![coord! {x: 20.0, y:0.5}, coord! {x:20.0, y:20.0}]),
375            LineString::new(vec![coord! {x:40.0, y:20.0}, coord! {x:40.5,y:40.5}]),
376            LineString::new(vec![coord! {x:0.0, y:0.0}, coord! {x:40.5,y:20.5}]),
377        ]);
378        let mut distance_unopt = 0.;
379        for i in 0..lines.0.len() - 1 {
380            let pt0 = lines.0[i].0.last().unwrap();
381            let pt1 = lines.0[i + 1].0.first().unwrap();
382            let ls0 = pt0.euclidean_distance(pt1);
383            distance_unopt = distance_unopt + ls0;
384        }
385        println!("UNOPT TRAVEL: {}", distance_unopt);
386        let opt = Optimizer::new(0.7, OptimizationStrategy::Greedy);
387        let out = opt.optimize(&lines);
388        println!("OUT OPT: ");
389        for pt in &out.0 {
390            println!("PT: {:?}", pt);
391        }
392        let mut distance_opt = 0.;
393        for i in 0..out.0.len() - 1 {
394            let pt0 = out.0[i].0.last().unwrap();
395            let pt1 = out.0[i + 1].0.first().unwrap();
396            let ls0 = pt0.euclidean_distance(pt1);
397            distance_opt = distance_opt + ls0;
398        }
399        println!("OPT TRAVEL: {}", distance_opt);
400    }
401}