hrdf_routing_engine/isochrone/
models.rs

1use chrono::NaiveDateTime;
2use geo::{Area, Contains, MultiPolygon};
3use hrdf_parser::Coordinates;
4use serde::Serialize;
5use strum_macros::EnumString;
6
7#[cfg(feature = "svg")]
8use geo::BoundingRect;
9#[cfg(feature = "svg")]
10use std::error::Error;
11use std::fmt::Display;
12#[cfg(feature = "svg")]
13use svg::Document;
14#[cfg(feature = "svg")]
15use svg::node::element::Polygon as SvgPolygon;
16
17use super::utils::{multi_polygon_to_lv95, wgs84_to_lv95};
18
19#[derive(Debug, Serialize, Default)]
20pub struct IsochroneMap {
21    isochrones: Vec<Isochrone>,
22    areas: Vec<f64>,
23    max_distances: Vec<((f64, f64), f64)>,
24    departure_stop_coord: Coordinates,
25    departure_at: NaiveDateTime,
26    bounding_box: ((f64, f64), (f64, f64)),
27}
28
29impl IsochroneMap {
30    pub fn new(
31        isochrones: Vec<Isochrone>,
32        areas: Vec<f64>,
33        max_distances: Vec<((f64, f64), f64)>,
34        departure_stop_coord: Coordinates,
35        departure_at: NaiveDateTime,
36        bounding_box: ((f64, f64), (f64, f64)),
37    ) -> Self {
38        Self {
39            isochrones,
40            areas,
41            max_distances,
42            departure_stop_coord,
43            departure_at,
44            bounding_box,
45        }
46    }
47
48    pub fn compute_areas(&self) -> Vec<f64> {
49        self.isochrones.iter().map(|i| i.compute_area()).collect()
50    }
51
52    pub fn compute_max_distances(&self, c: Coordinates) -> Vec<((f64, f64), f64)> {
53        self.isochrones
54            .iter()
55            .map(|i| i.compute_max_distance(c))
56            .collect()
57    }
58
59    pub fn compute_max_distance(&self, c: Coordinates) -> ((f64, f64), f64) {
60        self.compute_max_distances(c).into_iter().fold(
61            ((f64::MIN, f64::MIN), f64::MIN),
62            |((m_x, m_y), max), ((x, y), v)| {
63                if v > max {
64                    ((x, y), v)
65                } else {
66                    ((m_x, m_y), max)
67                }
68            },
69        )
70    }
71
72    /// Computes the area of the higher isochrone
73    pub fn compute_max_area(&self) -> f64 {
74        self.compute_areas().iter().fold(
75            f64::MIN,
76            |max_area, area| if *area > max_area { *area } else { max_area },
77        )
78    }
79
80    pub fn get_polygons(&self) -> Vec<MultiPolygon> {
81        let mut polygons = self
82            .isochrones
83            .iter()
84            .map(|i| i.polygons().clone())
85            .collect::<Vec<_>>();
86
87        let polygons_original = polygons.clone();
88
89        for i in 0..polygons.len() - 1 {
90            for p_ext in &mut polygons[i + 1] {
91                for p_int in &polygons_original[i] {
92                    if p_ext.contains(p_int) {
93                        p_ext.interiors_push(p_int.exterior().clone());
94                    }
95                }
96            }
97        }
98
99        polygons
100    }
101
102    pub fn departure_at(&self) -> NaiveDateTime {
103        self.departure_at
104    }
105
106    #[cfg(feature = "svg")]
107    pub fn write_svg(
108        &self,
109        path: &str,
110        scale_factor: f64,
111        c: Option<Coordinates>,
112    ) -> Result<(), Box<dyn Error>> {
113        const HEXES: [&str; 6] = [
114            "#36AB68", // Nearest.
115            "#91CF60", //
116            "#D7FF67", //
117            "#FFD767", //
118            "#FC8D59", //
119            "#E2453C", // Furthest.
120        ];
121        use svg::node::element::Line;
122
123        let polys = self
124            .get_polygons()
125            .into_iter()
126            .map(|m| multi_polygon_to_lv95(&m))
127            .collect::<Vec<_>>();
128        let areas = self.compute_areas();
129        let max_distances = if let Some(coord) = c {
130            self.compute_max_distances(coord)
131                .into_iter()
132                .map(Some)
133                .collect()
134        } else {
135            vec![None; areas.len()]
136        };
137
138        let bounding_rect = polys
139            .last()
140            .expect("MultiPolygons Vec is empty")
141            .bounding_rect()
142            .expect("Unable to find bounding rectangle");
143        let (min_x, min_y) = bounding_rect.min().x_y();
144        let (max_x, max_y) = bounding_rect.max().x_y();
145        let mut document = polys
146            .into_iter()
147            .rev()
148            .enumerate()
149            .zip(areas.into_iter().rev())
150            .fold(
151                Document::new().set(
152                    "viewBox",
153                    (
154                        min_x * scale_factor,
155                        min_y * scale_factor,
156                        (max_x - min_x) * scale_factor,
157                        (max_y - min_y) * scale_factor,
158                    ),
159                ),
160                |mut doc, ((num, pi), _area)| {
161                    doc = pi.iter().fold(doc, |doc_nested, p| {
162                        let points_ext = p
163                            .exterior()
164                            .coords()
165                            .map(|coord| {
166                                format!(
167                                    "{},{}",
168                                    coord.x * scale_factor,
169                                    (min_y + (max_y - coord.y)) * scale_factor
170                                )
171                            })
172                            .collect::<Vec<_>>();
173
174                        doc_nested.add(
175                            SvgPolygon::new()
176                                .set("fill", HEXES[num])
177                                .set("stroke", "black")
178                                .set("points", points_ext.join(" ")),
179                        )
180                    });
181                    doc
182                },
183            );
184        document = max_distances
185            .into_iter()
186            .rev()
187            .fold(document, |mut doc, dist| {
188                if let Some(coord) = c {
189                    if let Some(((x, y), _)) = dist {
190                        doc = doc.add(
191                            Line::new()
192                                .set("x1", x * scale_factor)
193                                .set("y1", (min_y + (max_y - y)) * scale_factor)
194                                .set("x2", coord.easting().unwrap() * scale_factor)
195                                .set(
196                                    "y2",
197                                    (min_y + (max_y - coord.northing().unwrap())) * scale_factor,
198                                )
199                                .set("stroke", "black"),
200                        );
201                        doc
202                    } else {
203                        doc
204                    }
205                } else {
206                    doc
207                }
208            });
209        svg::save(path, &document)?;
210        Ok(())
211    }
212}
213
214#[derive(Debug, Serialize)]
215pub struct Isochrone {
216    polygons: MultiPolygon,
217    time_limit: u32, // In minutes.
218}
219
220impl Isochrone {
221    pub fn new(polygons: MultiPolygon, time_limit: u32) -> Self {
222        Self {
223            polygons,
224            time_limit,
225        }
226    }
227
228    /// Transforms the isochrone polygons into geo::MultiPolygons to be able to use various
229    /// functionalities of the crate. The polygons are in lv95 coordinates
230    pub fn polygons(&self) -> &MultiPolygon {
231        &self.polygons
232    }
233
234    pub fn compute_area(&self) -> f64 {
235        multi_polygon_to_lv95(self.polygons())
236            .iter()
237            .map(|p| p.unsigned_area())
238            .sum()
239    }
240
241    /// Computes the max distance from all the points in the isochrone to the c Coord.
242    /// The distance is given in meters and the position in LV95 coordinates
243    pub fn compute_max_distance(&self, c: Coordinates) -> ((f64, f64), f64) {
244        self.polygons().iter().flat_map(|p| p.exterior()).fold(
245            ((f64::MIN, f64::MIN), f64::MIN),
246            |((o_x, o_y), max), coord| {
247                let (cx_lv95, cy_lv95) = wgs84_to_lv95(coord.x, coord.y);
248                let (c_x, c_y) = if let (Some(x), Some(y)) = (c.easting(), c.northing()) {
249                    (x, y)
250                } else {
251                    wgs84_to_lv95(c.latitude().unwrap(), c.longitude().unwrap())
252                };
253                let dist = f64::sqrt(f64::powi(c_x - cx_lv95, 2) + f64::powi(c_y - cy_lv95, 2));
254                if dist > max {
255                    ((cx_lv95, cy_lv95), dist)
256                } else {
257                    ((o_x, o_y), max)
258                }
259            },
260        )
261    }
262}
263
264#[derive(Debug, EnumString, PartialEq, Clone, Copy)]
265pub enum DisplayMode {
266    #[strum(serialize = "circles")]
267    Circles,
268    #[strum(serialize = "contour_line")]
269    ContourLine,
270}
271
272impl Display for DisplayMode {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        match self {
275            Self::Circles => write!(f, "circles"),
276            Self::ContourLine => write!(f, "contour_line"),
277        }
278    }
279}