hrdf_routing_engine/
isochrone.rs

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
44/// Computes the best isochrone in [departure_at - delta_time; departure_at + delta_time)
45/// Best is defined by the maximal surface covered by the largest isochrone
46pub 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/// Computes the worst isochrone in [departure_at - delta_time; departure_at + delta_time)
131/// Best is defined by the maximal surface covered by the largest isochrone
132#[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/// Computes the average isochrone.
218/// The point of origin is used to find the departure stop (the nearest stop).
219/// The departure date and time must be within the timetable period.
220#[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    // If there is no departue stop found we just use the default
247    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/// Computes the isochrones.
362/// The point of origin is used to find the departure stop (the nearest stop).
363/// The departure date and time must be within the timetable period.
364#[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    // If there is no departue stop found we just use the default
390    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    // We get only the stop coordinates
415    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        // The stop list cannot be empty.
519        .unwrap()
520}
521
522/// Each coordinate should be kept only once with the minimum duration associated
523fn 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            // We want the shortest trip duration to be kept only
539            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}