hrdf_routing_engine/isochrone/
models.rs

1use geo::{Area, Contains, LineString, MultiPolygon, Polygon};
2use hrdf_parser::Coordinates;
3use serde::Serialize;
4use strum_macros::EnumString;
5
6#[cfg(feature = "svg")]
7use geo::BoundingRect;
8#[cfg(feature = "svg")]
9use std::error::Error;
10#[cfg(feature = "svg")]
11use svg::node::element::Polygon as SvgPolygon;
12#[cfg(feature = "svg")]
13use svg::Document;
14
15use super::utils::wgs84_to_lv95;
16
17#[derive(Debug, Serialize)]
18pub struct IsochroneMap {
19    isochrones: Vec<Isochrone>,
20    departure_stop_coord: Coordinates,
21    bounding_box: ((f64, f64), (f64, f64)),
22}
23
24impl IsochroneMap {
25    pub fn new(
26        isochrones: Vec<Isochrone>,
27        departure_stop_coord: Coordinates,
28        bounding_box: ((f64, f64), (f64, f64)),
29    ) -> Self {
30        Self {
31            isochrones,
32            departure_stop_coord,
33            bounding_box,
34        }
35    }
36
37    pub fn compute_areas(&self) -> Vec<f64> {
38        self.isochrones.iter().map(|i| i.compute_area()).collect()
39    }
40
41    pub fn get_polygons(&self) -> Vec<MultiPolygon> {
42        let mut polygons = self
43            .isochrones
44            .iter()
45            .map(|i| i.to_polygons())
46            .collect::<Vec<_>>();
47
48        let polygons_original = polygons.clone();
49
50        for i in 0..polygons.len() - 1 {
51            for p_ext in &mut polygons[i + 1] {
52                for p_int in &polygons_original[i] {
53                    if p_ext.contains(p_int) {
54                        p_ext.interiors_push(p_int.exterior().clone());
55                    }
56                }
57            }
58        }
59
60        polygons
61    }
62
63    #[cfg(feature = "svg")]
64    pub fn write_svg(&self, path: &str) -> Result<(), Box<dyn Error>> {
65        let polys = self.get_polygons();
66
67        let bounding_rect = polys.last().unwrap().bounding_rect().unwrap();
68        let (min_x, min_y) = bounding_rect.min().x_y();
69        let (max_x, max_y) = bounding_rect.max().x_y();
70        let document = polys.iter().fold(
71            Document::new().set(
72                "viewBox",
73                (
74                    min_x / 100.0,
75                    min_y / 100.0,
76                    max_x / 100.0 - min_x / 100.0,
77                    max_y / 100.0 - min_y / 100.0,
78                ),
79            ),
80            |mut doc, pi| {
81                doc = pi.iter().fold(doc, |doc_nested, p| {
82                    let points_ext = p
83                        .exterior()
84                        .coords()
85                        .map(|coord| {
86                            format!(
87                                "{},{}",
88                                coord.x / 100.0,
89                                (min_y + (max_y - coord.y)) / 100.0
90                            )
91                        })
92                        .collect::<Vec<_>>();
93
94                    doc_nested.add(
95                        SvgPolygon::new()
96                            .set("fill", "none")
97                            .set("stroke", "red")
98                            .set("points", points_ext.join(" ")),
99                    )
100                });
101                doc
102            },
103        );
104        svg::save(format!("{path}"), &document)?;
105        Ok(())
106    }
107}
108
109#[derive(Debug, Serialize)]
110pub struct Isochrone {
111    polygons: Vec<Vec<Coordinates>>,
112    time_limit: u32, // In minutes.
113}
114
115impl Isochrone {
116    pub fn new(polygons: Vec<Vec<Coordinates>>, time_limit: u32) -> Self {
117        Self {
118            polygons,
119            time_limit,
120        }
121    }
122
123    /// Transforms the isochrone polygons into geo::MultiPolygons to be able to use various
124    /// functionalities of the crate
125    pub fn to_polygons(&self) -> MultiPolygon {
126        self.polygons
127            .iter()
128            .map(|p| {
129                Polygon::new(
130                    LineString::from(
131                        p.iter()
132                            .map(|c| {
133                                if let (Some(x), Some(y)) = (c.easting(), c.northing()) {
134                                    (x, y)
135                                } else {
136                                    wgs84_to_lv95(c.latitude().unwrap(), c.longitude().unwrap())
137                                }
138                            })
139                            .collect::<Vec<_>>(),
140                    ),
141                    vec![],
142                )
143            })
144            .collect()
145    }
146
147    pub fn compute_area(&self) -> f64 {
148        self.to_polygons().iter().map(|p| p.unsigned_area()).sum()
149    }
150}
151
152#[derive(Debug, EnumString, PartialEq, Clone, Copy)]
153pub enum DisplayMode {
154    #[strum(serialize = "circles")]
155    Circles,
156    #[strum(serialize = "contour_line")]
157    ContourLine,
158}