hrdf_routing_engine/isochrone/
models.rs1use 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 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", "#91CF60", "#D7FF67", "#FFD767", "#FC8D59", "#E2453C", ];
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, }
219
220impl Isochrone {
221 pub fn new(polygons: MultiPolygon, time_limit: u32) -> Self {
222 Self {
223 polygons,
224 time_limit,
225 }
226 }
227
228 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 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}