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::info!(
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::info!(
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::info!(
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::info!(
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::info!(
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 dx = 100.0;
302    let mut grids = data
303        .into_iter()
304        .map(|d| contour_line::create_grid(&d, bounding_box, time_limit, dx, 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 = MultiPolygon(polygons.into_iter().collect());
338            let polygons = polygons.difference(excluded_polygons);
339            Isochrone::new(polygons, current_time_limit.num_minutes() as u32)
340        })
341        .collect::<Vec<_>>();
342
343    let areas = isochrones.iter().map(|i| i.compute_area()).collect();
344    let max_distances = isochrones
345        .iter()
346        .map(|i| {
347            let ((x, y), max) = i.compute_max_distance(departure_coord_lv95);
348            let (w_x, w_y) = lv95_to_wgs84(x, y);
349            ((w_x, w_y), max)
350        })
351        .collect();
352
353    if verbose {
354        log::info!(
355            "Time for finding the isochrones : {:.2?}",
356            start_time.elapsed()
357        );
358    }
359    IsochroneMap::new(
360        isochrones,
361        areas,
362        max_distances,
363        departure_coord,
364        departure_at,
365        convert_bounding_box_to_wgs84(bounding_box),
366    )
367}
368
369/// Computes the isochrones.
370/// The point of origin is used to find the departure stop (the nearest stop).
371/// The departure date and time must be within the timetable period.
372#[allow(clippy::too_many_arguments)]
373pub fn compute_isochrones(
374    hrdf: &Hrdf,
375    excluded_polygons: &MultiPolygon,
376    isochrone_args: IsochroneArgs,
377    display_mode: IsochroneDisplayMode,
378    num_threads: usize,
379) -> IsochroneMap {
380    let IsochroneArgs {
381        latitude,
382        longitude,
383        departure_at,
384        time_limit,
385        interval: isochrone_interval,
386        max_num_explorable_connections,
387        num_starting_points,
388        verbose,
389    } = isochrone_args;
390
391    if verbose {
392        log::info!(
393            "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}",
394            time_limit.num_minutes(),
395            isochrone_interval.num_minutes()
396        );
397    }
398    // If there is no departue stop found we just use the default
399    let departure_coord = Coordinates::new(CoordinateSystem::WGS84, longitude, latitude);
400
401    let (easting, northing) = wgs84_to_lv95(latitude, longitude);
402    let departure_coord_lv95 = Coordinates::new(CoordinateSystem::LV95, easting, northing);
403
404    let start_time = Instant::now();
405
406    let routes = compute_routes_from_origin(
407        hrdf,
408        latitude,
409        longitude,
410        departure_at,
411        time_limit,
412        num_starting_points,
413        num_threads,
414        max_num_explorable_connections,
415        verbose,
416    );
417
418    if verbose {
419        log::info!("Time for finding the routes : {:.2?}", start_time.elapsed());
420    }
421
422    let start_time = Instant::now();
423
424    // We get only the stop coordinates
425    let data = unique_coordinates_from_routes(&routes, departure_at);
426
427    let bounding_box = get_bounding_box(&data, time_limit);
428    let dx = 100.0;
429
430    let grid = if display_mode == models::DisplayMode::ContourLine {
431        Some(contour_line::create_grid(
432            &data,
433            bounding_box,
434            time_limit,
435            dx,
436            num_threads,
437        ))
438    } else {
439        None
440    };
441
442    let isochrone_count = time_limit.num_minutes() / isochrone_interval.num_minutes();
443    let isochrones = (0..isochrone_count)
444        .map(|i| {
445            let current_time_limit = Duration::minutes(isochrone_interval.num_minutes() * (i + 1));
446            // let prev_time_limit = Duration::minutes(isochrone_interval.num_minutes() * i);
447            let prev_time_limit = Duration::minutes(0);
448
449            let polygons = match display_mode {
450                IsochroneDisplayMode::Circles => {
451                    let num_points_circle = 6;
452                    circles::get_polygons(
453                        &data,
454                        current_time_limit,
455                        prev_time_limit,
456                        num_points_circle,
457                        num_threads,
458                    )
459                }
460                IsochroneDisplayMode::ContourLine => {
461                    let (grid, num_points_x, num_points_y, dx) = grid.as_ref().unwrap();
462                    contour_line::get_polygons(
463                        grid,
464                        *num_points_x,
465                        *num_points_y,
466                        bounding_box.0,
467                        current_time_limit,
468                        *dx,
469                    )
470                }
471            };
472            // let polygons = MultiPolygon(polygons.into_iter().collect());
473            let polygons = polygons.difference(excluded_polygons);
474
475            Isochrone::new(polygons, current_time_limit.num_minutes() as u32)
476        })
477        .collect::<Vec<_>>();
478
479    let areas = isochrones.iter().map(|i| i.compute_area()).collect();
480    let max_distances = isochrones
481        .iter()
482        .map(|i| {
483            let ((x, y), max) = i.compute_max_distance(departure_coord_lv95);
484            let (w_x, w_y) = lv95_to_wgs84(x, y);
485            ((w_x, w_y), max)
486        })
487        .collect();
488
489    if verbose {
490        log::info!(
491            "Time for finding the isochrones : {:.2?}",
492            start_time.elapsed()
493        );
494    }
495    IsochroneMap::new(
496        isochrones,
497        areas,
498        max_distances,
499        departure_coord,
500        departure_at,
501        convert_bounding_box_to_wgs84(bounding_box),
502    )
503}
504
505#[allow(dead_code)]
506fn find_nearest_stop(
507    data_storage: &DataStorage,
508    origin_point_latitude: f64,
509    origin_point_longitude: f64,
510) -> &Stop {
511    data_storage
512        .stops()
513        .entries()
514        .into_iter()
515        .filter(|stop| stop.wgs84_coordinates().is_some())
516        .min_by(|a, b| {
517            let coord_1 = a.wgs84_coordinates().unwrap();
518            let distance_1 = haversine_distance(
519                origin_point_latitude,
520                origin_point_longitude,
521                coord_1.latitude().expect("Wrong coordinate system"),
522                coord_1.longitude().expect("Wrong coordinate system"),
523            );
524
525            let coord_2 = b.wgs84_coordinates().unwrap();
526            let distance_2 = haversine_distance(
527                origin_point_latitude,
528                origin_point_longitude,
529                coord_2.latitude().expect("Wrong coordinate system"),
530                coord_2.longitude().expect("Wrong coordinate system"),
531            );
532
533            distance_1.partial_cmp(&distance_2).unwrap()
534        })
535        // The stop list cannot be empty.
536        .unwrap()
537}
538
539/// Each coordinate should be kept only once with the minimum duration associated
540fn unique_coordinates_from_routes(
541    routes: &[Route],
542    departure_at: NaiveDateTime,
543) -> Vec<(Coordinates, Duration)> {
544    let mut coordinates_duration: HashMap<i32, (Coordinates, chrono::Duration)> = HashMap::new();
545    for route in routes {
546        let arrival_stop = route.sections().last().expect("Route sections was empty");
547        let arrival_stop_id = arrival_stop.arrival_stop_id();
548        let arrival_stop_coords = if let Some(c) = arrival_stop.arrival_stop_lv95_coordinates() {
549            c
550        } else {
551            continue;
552        };
553        let new_duration = route.arrival_at() - departure_at;
554        if let Some((_, duration)) = coordinates_duration.get_mut(&arrival_stop_id) {
555            // We want the shortest trip duration to be kept only
556            if new_duration < *duration {
557                *duration = new_duration;
558            }
559        } else {
560            let _ =
561                coordinates_duration.insert(arrival_stop_id, (arrival_stop_coords, new_duration));
562        }
563    }
564    coordinates_duration.into_values().collect()
565}
566
567fn get_bounding_box(
568    data: &[(Coordinates, Duration)],
569    time_limit: Duration,
570) -> ((f64, f64), (f64, f64)) {
571    let min_x = data
572        .iter()
573        .fold(f64::INFINITY, |result, &(coord, duration)| {
574            let candidate = coord.easting().expect("Wrong coordinate system")
575                - time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
576            f64::min(result, candidate)
577        });
578
579    let max_x = data
580        .iter()
581        .fold(f64::NEG_INFINITY, |result, &(coord, duration)| {
582            let candidate = coord.easting().expect("Wrong coordinate system")
583                + time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
584            f64::max(result, candidate)
585        });
586
587    let min_y = data
588        .iter()
589        .fold(f64::INFINITY, |result, &(coord, duration)| {
590            let candidate = coord.northing().expect("Wrong coordinate system")
591                - time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
592            f64::min(result, candidate)
593        });
594
595    let max_y = data
596        .iter()
597        .fold(f64::NEG_INFINITY, |result, &(coord, duration)| {
598            let candidate = coord.northing().expect("Wrong coordinate system")
599                + time_to_distance(time_limit - duration, WALKING_SPEED_IN_KILOMETERS_PER_HOUR);
600            f64::max(result, candidate)
601        });
602
603    ((min_x, min_y), (max_x, max_y))
604}
605
606fn convert_bounding_box_to_wgs84(
607    bounding_box: ((f64, f64), (f64, f64)),
608) -> ((f64, f64), (f64, f64)) {
609    (
610        lv95_to_wgs84(bounding_box.0.0, bounding_box.0.1),
611        lv95_to_wgs84(bounding_box.1.0, bounding_box.1.1),
612    )
613}