1mod circles;
2mod constants;
3mod contour_line;
4pub(crate) mod externals;
5mod models;
6pub(crate) mod utils;
7
8use std::collections::HashMap;
9use std::time::Instant;
10
11use crate::isochrone::utils::haversine_distance;
12use crate::routing::Route;
13use crate::routing::compute_routes_from_origin;
14use constants::WALKING_SPEED_IN_KILOMETERS_PER_HOUR;
15use geo::BooleanOps;
16use geo::MultiPolygon;
17use hrdf_parser::{CoordinateSystem, Coordinates, DataStorage, Hrdf, Stop};
18pub use models::DisplayMode as IsochroneDisplayMode;
19pub use models::IsochroneMap;
20
21use chrono::{Duration, NaiveDateTime};
22
23use models::Isochrone;
24use rayon::iter::IntoParallelRefIterator;
25use rayon::iter::ParallelIterator;
26use utils::lv95_to_wgs84;
27use utils::time_to_distance;
28
29use self::utils::NaiveDateTimeRange;
30use self::utils::wgs84_to_lv95;
31
32#[derive(Debug, Clone)]
33pub struct IsochroneArgs {
34 pub latitude: f64,
35 pub longitude: f64,
36 pub departure_at: NaiveDateTime,
37 pub time_limit: Duration,
38 pub interval: Duration,
39 pub max_num_explorable_connections: i32,
40 pub num_starting_points: usize,
41 pub verbose: bool,
42}
43
44pub fn compute_optimal_isochrones(
47 hrdf: &Hrdf,
48 excluded_polygons: &MultiPolygon,
49 isochrone_args: IsochroneArgs,
50 delta_time: Duration,
51 display_mode: models::DisplayMode,
52) -> IsochroneMap {
53 let IsochroneArgs {
54 latitude,
55 longitude,
56 departure_at,
57 time_limit,
58 interval: isochrone_interval,
59 max_num_explorable_connections,
60 num_starting_points,
61 verbose,
62 } = isochrone_args;
63
64 if verbose {
65 log::info!(
66 "longitude: {longitude}, latitude: {latitude}, departure_at: {departure_at}, time_limit: {}, isochrone_interval: {}, delta_time: {}, display_mode: {display_mode:?}, verbose: {verbose}",
67 time_limit.num_minutes(),
68 isochrone_interval.num_minutes(),
69 delta_time.num_minutes(),
70 );
71 }
72 let start_time = Instant::now();
73 let min_date_time = departure_at - delta_time;
74 let max_date_time = departure_at + delta_time;
75
76 let (isochrone_map, _) = NaiveDateTimeRange::new(
77 min_date_time + Duration::minutes(1),
78 max_date_time,
79 Duration::minutes(1),
80 )
81 .into_iter()
82 .collect::<Vec<_>>()
83 .par_iter()
84 .fold(
85 || (IsochroneMap::default(), f64::MIN),
86 |(iso_max, area_max), dep| {
87 let isochrone = compute_isochrones(
88 hrdf,
89 excluded_polygons,
90 IsochroneArgs {
91 latitude,
92 longitude,
93 departure_at: *dep,
94 time_limit,
95 interval: isochrone_interval,
96 max_num_explorable_connections,
97 num_starting_points,
98 verbose,
99 },
100 display_mode,
101 );
102 let curr_area = isochrone.compute_max_area();
103 if curr_area > area_max {
104 (isochrone, curr_area)
105 } else {
106 (iso_max, area_max)
107 }
108 },
109 )
110 .reduce(
111 || (IsochroneMap::default(), f64::MIN),
112 |(iso_max, area_max), (iso, area)| {
113 if area > area_max {
114 (iso, area)
115 } else {
116 (iso_max, area_max)
117 }
118 },
119 );
120
121 if verbose {
122 log::info!(
123 "Time computing the optimal solution : {:.2?}",
124 start_time.elapsed()
125 );
126 }
127 isochrone_map
128}
129
130#[allow(clippy::too_many_arguments)]
133pub fn compute_worst_isochrones(
134 hrdf: &Hrdf,
135 excluded_polygons: &MultiPolygon,
136 isochrone_args: IsochroneArgs,
137 delta_time: Duration,
138 display_mode: models::DisplayMode,
139) -> IsochroneMap {
140 let IsochroneArgs {
141 latitude,
142 longitude,
143 departure_at,
144 time_limit,
145 interval: isochrone_interval,
146 max_num_explorable_connections,
147 num_starting_points,
148 verbose,
149 } = isochrone_args;
150
151 if verbose {
152 log::info!(
153 "longitude: {longitude}, latitude: {latitude}, departure_at: {departure_at}, time_limit: {}, isochrone_interval: {}, delta_time: {}, display_mode: {display_mode:?}, verbose: {verbose}",
154 time_limit.num_minutes(),
155 isochrone_interval.num_minutes(),
156 delta_time.num_minutes(),
157 );
158 }
159 let start_time = Instant::now();
160 let min_date_time = departure_at - delta_time;
161 let max_date_time = departure_at + delta_time;
162
163 let (isochrone_map, _) = NaiveDateTimeRange::new(
164 min_date_time + Duration::minutes(1),
165 max_date_time,
166 Duration::minutes(1),
167 )
168 .into_iter()
169 .collect::<Vec<_>>()
170 .par_iter()
171 .fold(
172 || (IsochroneMap::default(), f64::MAX),
173 |(iso_max, area_max), dep| {
174 let isochrone = compute_isochrones(
175 hrdf,
176 excluded_polygons,
177 IsochroneArgs {
178 latitude,
179 longitude,
180 departure_at: *dep,
181 time_limit,
182 interval: isochrone_interval,
183 max_num_explorable_connections,
184 num_starting_points,
185 verbose,
186 },
187 display_mode,
188 );
189 let curr_area = isochrone.compute_max_area();
190 if curr_area < area_max {
191 (isochrone, curr_area)
192 } else {
193 (iso_max, area_max)
194 }
195 },
196 )
197 .reduce(
198 || (IsochroneMap::default(), f64::MAX),
199 |(iso_max, area_max), (iso, area)| {
200 if area < area_max {
201 (iso, area)
202 } else {
203 (iso_max, area_max)
204 }
205 },
206 );
207
208 if verbose {
209 log::info!(
210 "Time computing the optimal solution : {:.2?}",
211 start_time.elapsed()
212 );
213 }
214 isochrone_map
215}
216
217#[allow(clippy::too_many_arguments)]
221pub fn compute_average_isochrones(
222 hrdf: &Hrdf,
223 excluded_polygons: &MultiPolygon,
224 isochrone_args: IsochroneArgs,
225 delta_time: Duration,
226) -> IsochroneMap {
227 let IsochroneArgs {
228 latitude,
229 longitude,
230 departure_at,
231 time_limit,
232 interval: isochrone_interval,
233 max_num_explorable_connections,
234 num_starting_points,
235 verbose,
236 } = isochrone_args;
237
238 if verbose {
239 log::info!(
240 "Computing average isochrone:\n longitude: {longitude}, latitude: {latitude}, departure_at: {departure_at}, time_limit: {}, isochrone_interval: {}, delta_time: {}, verbose: {verbose}",
241 time_limit.num_minutes(),
242 isochrone_interval.num_minutes(),
243 delta_time.num_minutes()
244 );
245 }
246 let departure_coord = Coordinates::new(CoordinateSystem::WGS84, longitude, latitude);
248
249 let (easting, northing) = wgs84_to_lv95(latitude, longitude);
250 let departure_coord_lv95 = Coordinates::new(CoordinateSystem::LV95, easting, northing);
251
252 let start_time = Instant::now();
253 let min_date_time = departure_at - delta_time;
254 let max_date_time = departure_at + delta_time;
255
256 let data = NaiveDateTimeRange::new(
257 min_date_time + Duration::minutes(1),
258 max_date_time,
259 Duration::minutes(1),
260 )
261 .into_iter()
262 .collect::<Vec<_>>()
263 .par_iter()
264 .map(|dep| {
265 let routes = compute_routes_from_origin(
266 hrdf,
267 latitude,
268 longitude,
269 *dep,
270 time_limit,
271 num_starting_points,
272 max_num_explorable_connections,
273 verbose,
274 );
275
276 unique_coordinates_from_routes(&routes, departure_at)
277 })
278 .collect::<Vec<_>>();
279 let bounding_box = data.iter().fold(
280 ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN)),
281 |cover_bb, d| {
282 let bb = get_bounding_box(d, time_limit);
283 let x0 = f64::min(cover_bb.0.0, bb.0.0);
284 let x1 = f64::max(cover_bb.1.0, bb.1.0);
285 let y0 = f64::min(cover_bb.0.1, bb.0.1);
286 let y1 = f64::max(cover_bb.1.1, bb.1.1);
287 ((x0, y0), (x1, y1))
288 },
289 );
290
291 let num_points = 1500;
292 let mut grids = data
293 .into_iter()
294 .map(|d| contour_line::create_grid(&d, bounding_box, time_limit, num_points))
295 .collect::<Vec<_>>();
296 let timesteps = grids.len();
297 let grid_ini = grids.pop().expect("Grids was empty");
298 let (total_grid, nx, ny, dx) =
299 grids
300 .into_iter()
301 .fold(grid_ini, |(total, nx, ny, dx), (g, _, _, _)| {
302 let new_grid = g
303 .into_iter()
304 .zip(total)
305 .map(|((lc, ld), (_, rd))| (lc, (rd + ld)))
306 .collect::<Vec<_>>();
307 (new_grid, nx, ny, dx)
308 });
309 let avg_grid = total_grid
310 .into_iter()
311 .map(|(c, d)| (c, d / timesteps as i32))
312 .collect::<Vec<_>>();
313 let isochrone_count = time_limit.num_minutes() / isochrone_interval.num_minutes();
314 let isochrones = (0..isochrone_count)
315 .map(|i| {
316 let current_time_limit = Duration::minutes(isochrone_interval.num_minutes() * (i + 1));
317
318 let polygons = contour_line::get_polygons(
319 &avg_grid,
320 nx,
321 ny,
322 bounding_box.0,
323 current_time_limit,
324 dx,
325 );
326
327 let polygons = polygons
328 .into_iter()
329 .map(|p| p.difference(excluded_polygons))
330 .fold(MultiPolygon::new(vec![]), |res, p| res.union(&p));
331 Isochrone::new(polygons, current_time_limit.num_minutes() as u32)
332 })
333 .collect::<Vec<_>>();
334
335 let areas = isochrones.iter().map(|i| i.compute_area()).collect();
336 let max_distances = isochrones
337 .iter()
338 .map(|i| {
339 let ((x, y), max) = i.compute_max_distance(departure_coord_lv95);
340 let (w_x, w_y) = lv95_to_wgs84(x, y);
341 ((w_x, w_y), max)
342 })
343 .collect();
344
345 if verbose {
346 log::info!(
347 "Time for finding the isochrones : {:.2?}",
348 start_time.elapsed()
349 );
350 }
351 IsochroneMap::new(
352 isochrones,
353 areas,
354 max_distances,
355 departure_coord,
356 departure_at,
357 convert_bounding_box_to_wgs84(bounding_box),
358 )
359}
360
361#[allow(clippy::too_many_arguments)]
365pub fn compute_isochrones(
366 hrdf: &Hrdf,
367 excluded_polygons: &MultiPolygon,
368 isochrone_args: IsochroneArgs,
369 display_mode: IsochroneDisplayMode,
370) -> IsochroneMap {
371 let IsochroneArgs {
372 latitude,
373 longitude,
374 departure_at,
375 time_limit,
376 interval: isochrone_interval,
377 max_num_explorable_connections,
378 num_starting_points,
379 verbose,
380 } = isochrone_args;
381
382 if verbose {
383 log::info!(
384 "longitude: {longitude}, latitude : {latitude}, departure_at: {departure_at}, time_limit: {}, isochrone_interval: {}, display_mode: {display_mode:?}, max_num_explorable_connections: {max_num_explorable_connections}, verbose: {verbose}",
385 time_limit.num_minutes(),
386 isochrone_interval.num_minutes()
387 );
388 }
389 let departure_coord = Coordinates::new(CoordinateSystem::WGS84, longitude, latitude);
391
392 let (easting, northing) = wgs84_to_lv95(latitude, longitude);
393 let departure_coord_lv95 = Coordinates::new(CoordinateSystem::LV95, easting, northing);
394
395 let start_time = Instant::now();
396
397 let routes = compute_routes_from_origin(
398 hrdf,
399 latitude,
400 longitude,
401 departure_at,
402 time_limit,
403 num_starting_points,
404 max_num_explorable_connections,
405 verbose,
406 );
407
408 if verbose {
409 log::info!("Time for finding the routes : {:.2?}", start_time.elapsed());
410 }
411
412 let start_time = Instant::now();
413
414 let data = unique_coordinates_from_routes(&routes, departure_at);
416
417 let bounding_box = get_bounding_box(&data, time_limit);
418 let num_points = 1500;
419
420 let grid = if display_mode == models::DisplayMode::ContourLine {
421 Some(contour_line::create_grid(
422 &data,
423 bounding_box,
424 time_limit,
425 num_points,
426 ))
427 } else {
428 None
429 };
430
431 let isochrone_count = time_limit.num_minutes() / isochrone_interval.num_minutes();
432 let isochrones = (0..isochrone_count)
433 .map(|i| {
434 let current_time_limit = Duration::minutes(isochrone_interval.num_minutes() * (i + 1));
435
436 let polygons = match display_mode {
437 IsochroneDisplayMode::Circles => {
438 let num_points_circle = 6;
439 circles::get_polygons(&data, current_time_limit, num_points_circle)
440 }
441 IsochroneDisplayMode::ContourLine => {
442 let (grid, num_points_x, num_points_y, dx) = grid.as_ref().unwrap();
443 contour_line::get_polygons(
444 grid,
445 *num_points_x,
446 *num_points_y,
447 bounding_box.0,
448 current_time_limit,
449 *dx,
450 )
451 }
452 };
453 let polygons = polygons
454 .into_iter()
455 .map(|p| p.difference(excluded_polygons))
456 .fold(MultiPolygon::new(vec![]), |res, p| res.union(&p));
457
458 Isochrone::new(polygons, current_time_limit.num_minutes() as u32)
459 })
460 .collect::<Vec<_>>();
461
462 let areas = isochrones.iter().map(|i| i.compute_area()).collect();
463 let max_distances = isochrones
464 .iter()
465 .map(|i| {
466 let ((x, y), max) = i.compute_max_distance(departure_coord_lv95);
467 let (w_x, w_y) = lv95_to_wgs84(x, y);
468 ((w_x, w_y), max)
469 })
470 .collect();
471
472 if verbose {
473 log::info!(
474 "Time for finding the isochrones : {:.2?}",
475 start_time.elapsed()
476 );
477 }
478 IsochroneMap::new(
479 isochrones,
480 areas,
481 max_distances,
482 departure_coord,
483 departure_at,
484 convert_bounding_box_to_wgs84(bounding_box),
485 )
486}
487
488#[allow(dead_code)]
489fn find_nearest_stop(
490 data_storage: &DataStorage,
491 origin_point_latitude: f64,
492 origin_point_longitude: f64,
493) -> &Stop {
494 data_storage
495 .stops()
496 .entries()
497 .into_iter()
498 .filter(|stop| stop.wgs84_coordinates().is_some())
499 .min_by(|a, b| {
500 let coord_1 = a.wgs84_coordinates().unwrap();
501 let distance_1 = haversine_distance(
502 origin_point_latitude,
503 origin_point_longitude,
504 coord_1.latitude().expect("Wrong coordinate system"),
505 coord_1.longitude().expect("Wrong coordinate system"),
506 );
507
508 let coord_2 = b.wgs84_coordinates().unwrap();
509 let distance_2 = haversine_distance(
510 origin_point_latitude,
511 origin_point_longitude,
512 coord_2.latitude().expect("Wrong coordinate system"),
513 coord_2.longitude().expect("Wrong coordinate system"),
514 );
515
516 distance_1.partial_cmp(&distance_2).unwrap()
517 })
518 .unwrap()
520}
521
522fn unique_coordinates_from_routes(
524 routes: &[Route],
525 departure_at: NaiveDateTime,
526) -> Vec<(Coordinates, Duration)> {
527 let mut coordinates_duration: HashMap<i32, (Coordinates, chrono::Duration)> = HashMap::new();
528 for route in routes {
529 let arrival_stop = route.sections().last().expect("Route sections was empty");
530 let arrival_stop_id = arrival_stop.arrival_stop_id();
531 let arrival_stop_coords = if let Some(c) = arrival_stop.arrival_stop_lv95_coordinates() {
532 c
533 } else {
534 continue;
535 };
536 let new_duration = route.arrival_at() - departure_at;
537 if let Some((_, duration)) = coordinates_duration.get_mut(&arrival_stop_id) {
538 if new_duration < *duration {
540 *duration = new_duration;
541 }
542 } else {
543 let _ =
544 coordinates_duration.insert(arrival_stop_id, (arrival_stop_coords, new_duration));
545 }
546 }
547 coordinates_duration.into_values().collect()
548}
549
550fn get_bounding_box(
551 data: &[(Coordinates, Duration)],
552 time_limit: Duration,
553) -> ((f64, f64), (f64, f64)) {
554 let min_x = data
555 .iter()
556 .fold(f64::INFINITY, |result, &(coord, duration)| {
557 let candidate = coord.easting().expect("Wrong coordinate system")
558 - time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
559 f64::min(result, candidate)
560 });
561
562 let max_x = data
563 .iter()
564 .fold(f64::NEG_INFINITY, |result, &(coord, duration)| {
565 let candidate = coord.easting().expect("Wrong coordinate system")
566 + time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
567 f64::max(result, candidate)
568 });
569
570 let min_y = data
571 .iter()
572 .fold(f64::INFINITY, |result, &(coord, duration)| {
573 let candidate = coord.northing().expect("Wrong coordinate system")
574 - time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
575 f64::min(result, candidate)
576 });
577
578 let max_y = data
579 .iter()
580 .fold(f64::NEG_INFINITY, |result, &(coord, duration)| {
581 let candidate = coord.northing().expect("Wrong coordinate system")
582 + time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
583 f64::max(result, candidate)
584 });
585
586 ((min_x, min_y), (max_x, max_y))
587}
588
589fn convert_bounding_box_to_wgs84(
590 bounding_box: ((f64, f64), (f64, f64)),
591) -> ((f64, f64), (f64, f64)) {
592 (
593 lv95_to_wgs84(bounding_box.0.0, bounding_box.0.1),
594 lv95_to_wgs84(bounding_box.1.0, bounding_box.1.1),
595 )
596}