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::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#[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#[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 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#[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 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 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 .unwrap()
533}
534
535fn 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 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}