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 pub departure_at: NaiveDateTime,
37 pub time_limit: Duration,
39 pub max_num_explorable_connections: i32,
41 pub num_starting_points: usize,
43 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
59pub 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#[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#[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 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#[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 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 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(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 = 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 .unwrap()
537}
538
539fn 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 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}