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